PoemForSmallFThings / NAIA /wd14_test_v2.py
baqu2213's picture
Upload wd14_test_v2.py
d026b24 verified
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext, ttk
from PIL import Image, ImageTk, ImageGrab
import threading
import os
import numpy as np
import pandas as pd
import cv2
import tempfile
import io
import json
import sys
import time # ์‹œ๊ฐ„ ์ธก์ •์šฉ
from datetime import datetime # ํด๋”๋ช… ์ƒ์„ฑ์šฉ
import subprocess # ํด๋” ์—ด๊ธฐ์šฉ
import win32clipboard
import random
# resource_path ํ•จ์ˆ˜๋Š” ๋ณ€๊ฒฝ ์—†์ด ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
def resource_path(relative_path):
""" PyInstaller์— ์˜ํ•ด ์ž„์‹œ ํด๋”์— ์ƒ์„ฑ๋œ ๋ฆฌ์†Œ์Šค์˜ ์ ˆ๋Œ€ ๊ฒฝ๋กœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. """
try:
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
class WD14TaggerGUI:
def __init__(self, root):
self.root = root
self.root.title("WD14 Auto Prompt Generator (aiart) v2")
self.root.minsize(680, 800)
self.root.resizable(True, True)
self.SETTINGS_FILE = "tagger_app_settings.json"
# --- ๋ณ€์ˆ˜ ์ดˆ๊ธฐํ™” ---
self.current_image_path = None
self.session = None
self.tags_df = None
self.is_generating = False
self.save_counter = 1
self.last_generated_image = None
self.temp_image_path = os.path.join(tempfile.gettempdir(), "wd14_temp_generated.png")
self.save_session_folder = datetime.now().strftime('%Y%m%d_%H%M')
# Tkinter ๋ณ€์ˆ˜
self.general_threshold_var = tk.DoubleVar(value=0.5)
self.char_threshold_var = tk.DoubleVar(value=0.85)
self.rm_c_var = tk.BooleanVar()
self.rm_color_var = tk.BooleanVar()
self.rm_clothes_var = tk.BooleanVar()
self.webui_mode_var = tk.BooleanVar()
self.instant_inference_var = tk.BooleanVar(value=False)
self.show_removed_tags_var = tk.BooleanVar(value=True)
self.show_ignore_tags_var = tk.BooleanVar(value=False)
self.prompt_active_var = tk.BooleanVar(value=False)
self.expand_ui_var = tk.BooleanVar(value=False)
self.sampler_var = tk.StringVar()
self.resolution_var = tk.StringVar()
self.steps_var = tk.StringVar(value="28")
self.scale_var = tk.StringVar(value="5.5")
self.seed_var = tk.StringVar(value="-1")
self.cfg_rescale_var = tk.StringVar(value="0.25")
self.nai_token_var = tk.StringVar()
self.model_var = tk.StringVar()
self.auto_save_var = tk.BooleanVar(value=True)
self.instant_generate_var = tk.BooleanVar(value=False)
self.auto_rating_tag_var = tk.BooleanVar(value=True)
self.character_prompts = []
self.char_check_vars = [tk.BooleanVar(value=False) for _ in range(3)]
self.char_check_vars[0].set(True)
self.last_generated_seed = random.randint(0, 9999999999)
self.init_filter_data()
self.setup_ui()
self.load_model()
self.show_initial_preview()
self.setup_drag_drop()
self.load_settings()
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
self.root.bind('<Control-v>', self.handle_paste)
def _create_thumbnail(self, pil_image, size=512):
"""PIL ์ด๋ฏธ์ง€๋ฅผ ๋ฐ›์•„ ํฐ ๋ฐฐ๊ฒฝ์˜ ์ •์‚ฌ๊ฐํ˜• ์ธ๋„ค์ผ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค."""
# 1. ์›๋ณธ ์ด๋ฏธ์ง€์˜ ๋„ˆ๋น„์™€ ๋†’์ด ์ค‘ ๋” ํฐ ์ชฝ์„ ๊ธฐ์ค€์œผ๋กœ ์ •์‚ฌ๊ฐํ˜• ์บ”๋ฒ„์Šค ์ƒ์„ฑ
bg_size = max(pil_image.size)
thumbnail_bg = Image.new('RGB', (bg_size, bg_size), 'white')
# 2. ์บ”๋ฒ„์Šค ์ค‘์•™์— ์›๋ณธ ์ด๋ฏธ์ง€ ๋ถ™์—ฌ๋„ฃ๊ธฐ
paste_x = (bg_size - pil_image.width) // 2
paste_y = (bg_size - pil_image.height) // 2
thumbnail_bg.paste(pil_image, (paste_x, paste_y))
# 3. ์ตœ์ข… ์ธ๋„ค์ผ ํฌ๊ธฐ๋กœ ๋ฆฌ์‚ฌ์ด์ฆˆ
thumbnail_bg.thumbnail((size, size), Image.Resampling.LANCZOS)
return thumbnail_bg
def _update_timer(self):
"""์ƒ์„ฑ ๋ฒ„ํŠผ์˜ ํƒ€์ด๋จธ๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค."""
if self.is_generating:
elapsed = time.time() - self.generation_start_time
self.generation_btn.config(text=f"์ƒ์„ฑ ์ค‘: {int(elapsed)}s")
self.root.after(1000, self._update_timer) # 1์ดˆ๋งˆ๋‹ค ๋ฐ˜๋ณต
def _copy_image_to_clipboard(self):
"""์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€๋ฅผ ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค (Windows ์ „์šฉ)."""
if self.last_generated_image is None:
messagebox.showinfo("์ •๋ณด", "๋ณต์‚ฌํ•  ์ด๋ฏธ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
return
if win32clipboard is None:
messagebox.showerror("์˜ค๋ฅ˜", "์ด๋ฏธ์ง€ ๋ณต์‚ฌ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด 'pywin32' ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.\n(pip install pywin32)")
return
if sys.platform == "win32":
try:
output = io.BytesIO()
self.last_generated_image.convert("RGB").save(output, "BMP")
data = output.getvalue()[14:]
output.close()
win32clipboard.OpenClipboard()
win32clipboard.EmptyClipboard()
win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
win32clipboard.CloseClipboard()
#messagebox.showinfo("์„ฑ๊ณต", "์ด๋ฏธ์ง€๋ฅผ ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌํ–ˆ์Šต๋‹ˆ๋‹ค.")
except Exception as e:
messagebox.showerror("๋ณต์‚ฌ ์‹คํŒจ", f"์ด๋ฏธ์ง€ ๋ณต์‚ฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
else:
messagebox.showinfo("์ •๋ณด", "์ด๋ฏธ์ง€ ๋ณต์‚ฌ ๊ธฐ๋Šฅ์€ Windows์—์„œ๋งŒ ์ง€์›๋ฉ๋‹ˆ๋‹ค.")
def _copy_file_object_to_clipboard(self):
"""์ž„์‹œ ์ €์žฅ๋œ ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ํด๋ฆฝ๋ณด๋“œ์— ํŒŒ์ผ ๊ฐ์ฒด๋กœ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค (Windows ์ „์šฉ)."""
if not os.path.exists(self.temp_image_path):
messagebox.showinfo("์ •๋ณด", "๋ณต์‚ฌํ•  ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค.")
return
if sys.platform == "win32":
try:
command = f"powershell -command \"Get-Item '{self.temp_image_path}' | Set-Clipboard\""
subprocess.run(command, check=True, shell=True, creationflags=subprocess.CREATE_NO_WINDOW)
#messagebox.showinfo("์„ฑ๊ณต", "ํŒŒ์ผ์„ ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌํ–ˆ์Šต๋‹ˆ๋‹ค.")
except Exception as e:
messagebox.showerror("๋ณต์‚ฌ ์‹คํŒจ", f"ํŒŒ์ผ ๋ณต์‚ฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
else:
messagebox.showinfo("์ •๋ณด", "ํŒŒ์ผ ๋ณต์‚ฌ ๊ธฐ๋Šฅ์€ Windows์—์„œ๋งŒ ์ง€์›๋ฉ๋‹ˆ๋‹ค.")
def _show_context_menu(self, event):
"""์ด๋ฏธ์ง€ ๋ ˆ์ด๋ธ” ์œ„์—์„œ ์šฐํด๋ฆญ ์‹œ ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค."""
if self.last_generated_image:
self.image_context_menu.tk_popup(event.x_root, event.y_root)
def _save_image_manual(self):
"""(์ˆ˜์ •๋จ) ์ด๋ฏธ์ง€๋ฅผ ์ž๋™ ์ €์žฅ ํด๋”์— ์ˆœ์ฐจ์ ์œผ๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค."""
if self.last_generated_image is None:
messagebox.showinfo("์ •๋ณด", "์ €์žฅํ•  ์ด๋ฏธ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
return
try:
save_dir = os.path.join("Output_WD14", self.save_session_folder)
os.makedirs(save_dir, exist_ok=True)
file_path = os.path.join(save_dir, f"{self.save_counter:05d}.png")
self.last_generated_image.save(file_path, "PNG")
#messagebox.showinfo("์„ฑ๊ณต", f"์ด๋ฏธ์ง€๋ฅผ {file_path}์— ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.")
self.save_counter += 1
self.save_btn.config(state=tk.DISABLED) # ์ €์žฅ ํ›„ ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™”
except Exception as e:
messagebox.showerror("์ €์žฅ ์‹คํŒจ", f"์ด๋ฏธ์ง€ ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
def _open_save_folder(self):
"""์ž๋™ ์ €์žฅ ํด๋”๋ฅผ ์—ฝ๋‹ˆ๋‹ค."""
session_path = os.path.join("Output_WD14", self.save_session_folder)
base_path = "Output_WD14"
try:
if os.path.isdir(session_path):
path_to_open = session_path
elif os.path.isdir(base_path):
path_to_open = base_path
else:
messagebox.showinfo("์ •๋ณด", f"'{base_path}' ํด๋”๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
return
if sys.platform == "win32":
os.startfile(os.path.realpath(path_to_open))
elif sys.platform == "darwin":
subprocess.Popen(["open", os.path.realpath(path_to_open)])
else:
subprocess.Popen(["xdg-open", os.path.realpath(path_to_open)])
except Exception as e:
messagebox.showerror("์˜ค๋ฅ˜", f"ํด๋”๋ฅผ ์—ฌ๋Š” ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
# Step 2 & 3 & 4์˜ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•  ๋ฉ”์„œ๋“œ
def toggle_expand_ui(self):
"""์ฒดํฌ๋ฐ•์Šค ์ƒํƒœ์— ๋”ฐ๋ผ ์šฐ์ธก UI์™€ '์ฆ‰์‹œ ์ƒ์„ฑ' ๋ฒ„ํŠผ์˜ ๊ฐ€์‹œ์„ฑ์„ ์ œ์–ดํ•ฉ๋‹ˆ๋‹ค."""
main_frame = self.right_pane.master
if self.expand_ui_var.get():
main_frame.grid_columnconfigure(1, weight=1, minsize=400)
self.right_pane.grid(row=0, column=1, sticky="nsew")
self.root.minsize(1220, 800)
# '์ฆ‰์‹œ ์ƒ์„ฑ' ์ฒดํฌ๋ฐ•์Šค ํ‘œ์‹œ
self.instant_generate_cb.pack(side=tk.LEFT, padx=(5,0))
else:
self.right_pane.grid_forget()
main_frame.grid_columnconfigure(1, weight=0, minsize=0)
self.root.minsize(680, 800)
# '์ฆ‰์‹œ ์ƒ์„ฑ' ์ฒดํฌ๋ฐ•์Šค ์ˆจ๊น€
self.instant_generate_cb.pack_forget()
# Step 3์˜ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•  ๋ฉ”์„œ๋“œ
def show_generation_preview(self):
"""์šฐ์ธก ํŒจ๋„์— 512x512 ํฐ์ƒ‰ ์ด๋ฏธ์ง€๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค."""
try:
white_image = Image.new('RGB', (512, 512), 'white')
photo = ImageTk.PhotoImage(white_image)
# PhotoImage ๊ฐ์ฒด์— ๋Œ€ํ•œ ์ฐธ์กฐ๋ฅผ ์œ ์ง€ํ•˜์—ฌ ๊ฐ€๋น„์ง€ ์ปฌ๋ ‰์…˜์„ ๋ฐฉ์ง€
self.generation_image_label.image = photo
self.generation_image_label.config(image=photo)
except Exception as e:
self.generation_image_label.config(text=f"์ด๋ฏธ์ง€ ํ‘œ์‹œ ์˜ค๋ฅ˜: {e}")
def random_seed(self):
if int(self.seed_var.get()) == -1:
self.seed_var.set(str(self.last_generated_seed))
else:
self.seed_var.set("-1")
def setup_ui(self):
# ... (๊ธฐ์กด setup_ui์˜ ์‹œ์ž‘ ๋ถ€๋ถ„์€ ๋™์ผ) ...
self.root.grid_columnconfigure(0, weight=1)
self.root.grid_rowconfigure(0, weight=1)
main_frame = ttk.Frame(self.root, padding="10")
main_frame.grid(row=0, column=0, sticky="nsew")
main_frame.grid_columnconfigure(0, weight=1)
main_frame.grid_columnconfigure(1, weight=1, minsize=400)
main_frame.grid_rowconfigure(0, weight=1)
# --- ์ขŒ์ธก UI ํŒจ๋„ ---
left_pane = ttk.Frame(main_frame)
left_pane.grid(row=0, column=0, sticky="nsew")
left_pane.grid_columnconfigure(0, weight=1)
left_pane.grid_rowconfigure(2, weight=4)
left_pane.grid_rowconfigure(3, weight=3)
# ... (์ขŒ์ธก ํŒจ๋„์˜ ๋ชจ๋“  UI ๊ตฌ์„ฑ์€ ๊ธฐ์กด๊ณผ ๋™์ผ) ...
load_frame = ttk.Frame(left_pane)
load_frame.grid(row=0, column=0, sticky="ew", pady=(0, 10))
self.load_btn = ttk.Button(load_frame, text="์ด๋ฏธ์ง€ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ", command=self.load_image)
self.load_btn.pack(side="left", padx=(0, 5))
self.clipboard_btn = ttk.Button(load_frame, text="์ด๋ฏธ์ง€ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ (ํด๋ฆฝ๋ณด๋“œ)", command=self.load_from_clipboard)
self.clipboard_btn.pack(side="left")
self.expand_ui_cb = ttk.Checkbutton(load_frame, text="์ƒ์„ฑ UI ํ™•์žฅ",
variable=self.expand_ui_var,
command=self.toggle_expand_ui)
self.expand_ui_cb.pack(side="left", padx=(10, 0))
content_frame = ttk.Frame(left_pane)
content_frame.grid(row=1, column=0, sticky="new")
content_frame.grid_columnconfigure(1, weight=1)
self.preview_frame = ttk.LabelFrame(content_frame, text="๋ฏธ๋ฆฌ๋ณด๊ธฐ", padding="10")
self.preview_frame.grid(row=0, column=0, sticky="n", padx=(0, 10))
self.image_label = ttk.Label(self.preview_frame)
self.image_label.grid(row=0, column=0)
settings_frame = ttk.LabelFrame(content_frame, text="์„ค์ •", padding="10")
settings_frame.grid(row=0, column=1, sticky="nsew")
settings_frame.grid_columnconfigure(1, weight=1)
threshold_frame = ttk.Frame(settings_frame)
threshold_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
threshold_frame.grid_columnconfigure(1, weight=1)
ttk.Label(threshold_frame, text="ํƒœ๊ทธ ์ž„๊ณ„๊ฐ’:").grid(row=0, column=0, sticky="w")
ttk.Spinbox(threshold_frame, from_=0.0, to=1.0, increment=0.05,
textvariable=self.general_threshold_var, width=15).grid(row=0, column=1, sticky="we", pady=(0, 5))
extract_control_frame = ttk.Frame(settings_frame)
extract_control_frame.grid(row=1, column=0, columnspan=2, pady=5, sticky="ew")
self.extract_btn = ttk.Button(extract_control_frame, text="ํƒœ๊ทธ ์ถ”์ถœ", command=self.extract_tags, state='disabled')
self.extract_btn.pack(side=tk.LEFT, padx=(0, 10))
self.instant_inference_cb = ttk.Checkbutton(extract_control_frame, text="์ฆ‰์‹œ ์ถ”๋ก ", variable=self.instant_inference_var)
self.instant_inference_cb.pack(side=tk.LEFT)
# '์ฆ‰์‹œ ์ƒ์„ฑ' ์ฒดํฌ๋ฐ•์Šค ์ƒ์„ฑ (์ดˆ๊ธฐ์—๋Š” ์ˆจ๊ฒจ์ง)
self.instant_generate_cb = ttk.Checkbutton(extract_control_frame, text="์ฆ‰์‹œ ์ƒ์„ฑ", variable=self.instant_generate_var)
filter_frame = ttk.LabelFrame(settings_frame, text="ํƒœ๊ทธ ํ•„ํ„ฐ๋ง", padding="5")
filter_frame.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(5, 0))
ttk.Checkbutton(filter_frame, text="์บ๋ฆญํ„ฐ ํŠน์ง•+์ด๋ฆ„ ์ œ๊ฑฐ",
variable=self.rm_c_var).pack(anchor="w")
ttk.Checkbutton(filter_frame, text="์˜๋ฅ˜ ์ƒ‰์ƒ ์ œ๊ฑฐ",
variable=self.rm_color_var).pack(anchor="w")
ttk.Checkbutton(filter_frame, text="WEBUI/Comfy ๋ชจ๋“œ",
variable=self.webui_mode_var).pack(anchor="w")
prompt_outer_frame = ttk.Frame(settings_frame)
prompt_outer_frame.grid(row=3, column=0, columnspan=2, sticky="ew", pady=(10, 0))
prompt_outer_frame.grid_columnconfigure(0, weight=1)
self.prompt_active_cb = ttk.Checkbutton(prompt_outer_frame, text="์„ ํ–‰/ํ›„ํ–‰ ํ”„๋กฌํ”„ํŠธ ํ™œ์„ฑ",
variable=self.prompt_active_var,
command=self.toggle_prompts)
self.prompt_active_cb.grid(row=0, column=0, sticky="w")
self.prompt_notebook = ttk.Notebook(prompt_outer_frame)
prefix_frame = ttk.Frame(self.prompt_notebook, padding=5)
self.prompt_notebook.add(prefix_frame, text="์„ ํ–‰ ํ”„๋กฌํ”„ํŠธ")
self.prefix_text = scrolledtext.ScrolledText(prefix_frame, height=3, width=30)
self.prefix_text.pack(fill="both", expand=True)
suffix_frame = ttk.Frame(self.prompt_notebook, padding=5)
self.prompt_notebook.add(suffix_frame, text="ํ›„ํ–‰ ํ”„๋กฌํ”„ํŠธ")
self.suffix_text = scrolledtext.ScrolledText(suffix_frame, height=3, width=30)
self.suffix_text.pack(fill="both", expand=True)
self.toggle_prompts()
result_frame = ttk.LabelFrame(left_pane, text="์ถ”์ถœ๋œ ํƒœ๊ทธ", padding="10")
result_frame.grid(row=2, column=0, sticky="nsew", pady=(10, 0))
result_frame.grid_columnconfigure(0, weight=1)
result_frame.grid_rowconfigure(0, weight=3)
result_frame.grid_rowconfigure(3, weight=2)
self.result_text = scrolledtext.ScrolledText(result_frame, height=7, wrap=tk.WORD)
self.result_text.grid(row=0, column=0, sticky="nsew")
self.result_text.tag_config("prompt", foreground="grey")
copy_frame = ttk.Frame(result_frame)
copy_frame.grid(row=1, column=0, pady=(10, 5), sticky="w")
ttk.Button(copy_frame, text="์ „์ฒด ๋ณต์‚ฌ", command=self.copy_all_tags).pack(side="left", padx=(0, 5))
ttk.Button(copy_frame, text="2๋ฒˆ ํƒœ๊ทธ๋ถ€ํ„ฐ ๋ณต์‚ฌ", command=self.copy_from_second).pack(side="left", padx=5)
ttk.Button(copy_frame, text="3๋ฒˆ ํƒœ๊ทธ๋ถ€ํ„ฐ ๋ณต์‚ฌ", command=self.copy_from_third).pack(side="left", padx=5)
auto_rating_cb = ttk.Checkbutton(copy_frame, text="nsfw/safe ํƒœ๊ทธ ์ž๋™ ๋ฐ˜์˜", variable=self.auto_rating_tag_var)
auto_rating_cb.pack(side="left", padx=10)
self.removed_tags_cb = ttk.Checkbutton(result_frame, text="์ œ๊ฑฐ๋œ ํƒœ๊ทธ ๋ณด๊ธฐ",
variable=self.show_removed_tags_var,
command=self.toggle_removed_tags)
self.removed_tags_cb.grid(row=2, column=0, sticky="w", pady=(10, 2))
self.removed_tags_text = scrolledtext.ScrolledText(result_frame, height=4, wrap=tk.WORD, state=tk.DISABLED)
self.removed_tags_text.tag_config("ignored", foreground="red")
self.toggle_removed_tags()
bottom_frame = ttk.Frame(left_pane)
bottom_frame.grid(row=3, column=0, sticky="nsew", pady=(10, 0))
bottom_frame.grid_columnconfigure(0, weight=1)
bottom_frame.grid_rowconfigure(1, weight=1)
bottom_control_frame = ttk.Frame(bottom_frame)
bottom_control_frame.grid(row=0, column=0, sticky="ew")
self.ignore_tags_cb = ttk.Checkbutton(bottom_control_frame, text="๋ฌด์‹œํ•  ํƒœ๊ทธ ํŽธ์ง‘ (์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„)",
variable=self.show_ignore_tags_var,
command=self.toggle_ignore_tags)
self.ignore_tags_cb.pack(side="left", anchor="w")
self.save_settings_btn = ttk.Button(bottom_control_frame, text="์„ค์ • ์ €์žฅ", command=self.save_settings)
self.save_settings_btn.pack(side="right", anchor="e")
self.ignore_tags_frame = ttk.Frame(bottom_frame)
self.ignore_tags_frame.grid_columnconfigure(0, weight=1)
self.ignore_tags_frame.grid_rowconfigure(0, weight=1)
self.ignore_tags_text = scrolledtext.ScrolledText(self.ignore_tags_frame, height=4, wrap=tk.WORD)
self.ignore_tags_text.grid(row=0, column=0, sticky="nsew")
self.toggle_ignore_tags()
# --- ์šฐ์ธก UI ํŒจ๋„ ---
self.right_pane = ttk.Frame(main_frame)
self.right_pane.grid_rowconfigure(0, weight=1)
self.right_pane.grid_rowconfigure(2, weight=0)
self.right_pane.grid_columnconfigure(0, weight=1)
generation_preview_frame = ttk.LabelFrame(self.right_pane, text="์ƒ์„ฑ ๋ฏธ๋ฆฌ๋ณด๊ธฐ", padding="5")
generation_preview_frame.grid(row=0, column=0, sticky="nsew")
generation_preview_frame.grid_columnconfigure(0, weight=1)
generation_preview_frame.grid_rowconfigure(0, weight=1)
self.generation_image_label = ttk.Label(generation_preview_frame, anchor="center")
self.generation_image_label.grid(row=0, column=0, sticky="nsew")
self.generation_image_label.bind("<Button-3>", self._show_context_menu) # ์šฐํด๋ฆญ ์ด๋ฒคํŠธ ๋ฐ”์ธ๋”ฉ
# ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด ์ƒ์„ฑ
self.image_context_menu = tk.Menu(self.root, tearoff=0)
self.image_context_menu.add_command(label="์ด๋ฏธ์ง€๋ฅผ ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌ", command=self._copy_image_to_clipboard)
self.image_context_menu.add_command(label="ํŒŒ์ผ์„ ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌ", command=self._copy_file_object_to_clipboard)
self.image_context_menu.add_command(label="๊ธฐ๋ณธ ๋ทฐ์–ด๋กœ ์ด๋ฏธ์ง€ ์—ด๊ธฐ", command=self._show_image_with_pil)
# generation_button_frame ๊ตฌ์„ฑ ๋ถ€๋ถ„ ์ˆ˜์ •
generation_button_frame = ttk.Frame(self.right_pane)
generation_button_frame.grid(row=1, column=0, sticky="ew", pady=(5, 0), padx=10)
self.folder_btn = ttk.Button(generation_button_frame, text="๐Ÿ“", width=2, command=self._open_save_folder)
self.folder_btn.pack(side="right")
self.save_btn = ttk.Button(generation_button_frame, text="Save ๐Ÿ’พ", width=8, command=self._save_image_manual)
self.save_btn.pack(side="right", padx=(5, 0))
self.copy_btn = ttk.Button(generation_button_frame, text=" Copy ๐Ÿ—ƒ๏ธ", width=8, command=self._copy_image_to_clipboard)
self.copy_btn.pack(side="right", padx=(5, 0))
self.generation_btn = ttk.Button(generation_button_frame, text="Generation", command=self.start_generation)
self.generation_btn.pack(side="left", fill="x", expand=True)
# --- Notebook ๋ฐ ํƒญ ์„ค์ • ---
self.settings_notebook = ttk.Notebook(self.right_pane)
self.settings_notebook.grid(row=2, column=0, sticky="nsew", pady=(5, 0), padx=10)
# <<<<<<< ํ•ต์‹ฌ ๋ณ€๊ฒฝ์ : ๋ชจ๋“  ํƒญ ํ”„๋ ˆ์ž„์„ ๋จผ์ € ์ƒ์„ฑ >>>>>>>>>
basic_settings_tab = ttk.Frame(self.settings_notebook, padding="10")
char_prompt_tab = ttk.Frame(self.settings_notebook, padding=(10, 10, 10, 0))
account_settings_tab = ttk.Frame(self.settings_notebook, padding="10")
# <<<<<<< ํ•ต์‹ฌ ๋ณ€๊ฒฝ์ : ์›ํ•˜๋Š” ์ˆœ์„œ๋Œ€๋กœ .add()๋งŒ ์‚ฌ์šฉ >>>>>>>>>
self.settings_notebook.add(basic_settings_tab, text="๊ธฐ๋ณธ์„ค์ •")
self.settings_notebook.add(char_prompt_tab, text="์บ๋ฆญํ„ฐ ํ”„๋กฌํ”„ํŠธ")
self.settings_notebook.add(account_settings_tab, text="NAI์„ค์ •")
# --- ์ด์ œ ๊ฐ ํƒญ์˜ ๋‚ด์šฉ์„ ์ฑ„์›๋‹ˆ๋‹ค ---
# 1. '๊ธฐ๋ณธ์„ค์ •' ํƒญ ๋‚ด์šฉ ๊ตฌ์„ฑ
# ... ('๊ธฐ๋ณธ์„ค์ •' ํƒญ์˜ ๋ชจ๋“  ์œ„์ ฏ ์ฝ”๋“œ๋Š” ์—ฌ๊ธฐ์— ๊ทธ๋Œ€๋กœ ์œ ์ง€) ...
basic_settings_tab.grid_columnconfigure(1, weight=1); basic_settings_tab.grid_columnconfigure(3, weight=1); basic_settings_tab.grid_columnconfigure(5, weight=1)
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"]
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])
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")
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")
resolution_list = ["1024 x 1024", "960 x 1088", "896 x 1152", "832 x 1216", "1088 x 960", "1152 x 896", "1216 x 832", "์ž๋™ ํ•ด์ƒ๋„"]
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])
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))
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))
uc_header_frame = ttk.Frame(basic_settings_tab)
uc_header_frame.grid(row=2, column=0, columnspan=6, sticky="ew", pady=(8, 2))
# 2. ์ปจํ…Œ์ด๋„ˆ ํ”„๋ ˆ์ž„ ๋‚ด๋ถ€์— ๊ธฐ์กด ๋ ˆ์ด๋ธ”๊ณผ ์‹ ๊ทœ ์ฒดํฌ๋ฐ•์Šค ๋ฐฐ์น˜
ttk.Label(uc_header_frame, text="Negative Prompt (UC):").pack(side="left")
auto_save_cb = ttk.Checkbutton(uc_header_frame, text="NAI ์ƒ์„ฑ ์ด๋ฏธ์ง€๋ฅผ ์ž๋™์œผ๋กœ ์ €์žฅ", variable=self.auto_save_var)
auto_save_cb.pack(side="right")
# UC ์ž…๋ ฅ์ฐฝ์€ ๋‹ค์Œ ํ–‰(row=3)์— ๋ฐฐ์น˜
self.uc_text = scrolledtext.ScrolledText(basic_settings_tab, height=4, wrap=tk.WORD)
self.uc_text.grid(row=3, column=0, columnspan=6, sticky="nsew")
# 2. '์บ๋ฆญํ„ฐ ํ”„๋กฌํ”„ํŠธ' ํƒญ ๋‚ด์šฉ ๊ตฌ์„ฑ
char_prompt_tab.grid_columnconfigure(0, weight=1)
for i in range(3):
# ๊ฐ ์บ๋ฆญํ„ฐ ๋ผ์ธ์„ ๋‹ด์„ ํ”„๋ ˆ์ž„
char_frame = ttk.Frame(char_prompt_tab, padding=(0, 0, 0, 5))
# <<<<<<< Step 1: ์ •์  ๊ทธ๋ฆฌ๋“œ ๋ฐฐ์น˜ >>>>>>>>>
char_frame.grid(row=i, column=0, sticky="ew", pady=2) # ์ฆ‰์‹œ ๊ณ ์ • ๋ฐฐ์น˜
# <<<<<<< Step 3: ๋‹จ์ผ ํ–‰ ๋ ˆ์ด์•„์›ƒ ๋ฐ 3:2 ๋น„์œจ ์„ค์ • >>>>>>>>>
# char_frame.grid_columnconfigure(2, weight=3) # P ์ž…๋ ฅ์ฐฝ ๊ฐ€๋กœ ๋น„์œจ
# char_frame.grid_columnconfigure(4, weight=2) # UC ์ž…๋ ฅ์ฐฝ ๊ฐ€๋กœ ๋น„์œจ
# ์œ„์ ฏ ์ƒ์„ฑ
check_btn = ttk.Checkbutton(char_frame, text=f"C{i+1}", variable=self.char_check_vars[i],
command=self.update_character_layout) # style ์ œ๊ฑฐ
prompt_label = ttk.Label(char_frame, text="P:")
prompt_text = scrolledtext.ScrolledText(char_frame, height=1, wrap=tk.WORD, width=46)
uc_label = ttk.Label(char_frame, text="UC:")
uc_text = scrolledtext.ScrolledText(char_frame, height=1, wrap=tk.WORD, width=24)
# ์œ„์ ฏ ๋ฐฐ์น˜ (๋‹จ์ผ ํ–‰ ๊ตฌ์กฐ)
check_btn.grid(row=0, column=0, sticky="w", padx=(0, 5))
prompt_label.grid(row=0, column=1, sticky="w", padx=(0, 2))
# <<<<<<< Step 3: sticky="ew"๋กœ ์ˆ˜์ • >>>>>>>>>
prompt_text.grid(row=0, column=2, sticky="ew")
uc_label.grid(row=0, column=3, sticky="w", padx=(5, 2))
uc_text.grid(row=0, column=4, sticky="ew") # sticky="ew"๋กœ ์ˆ˜์ •
self.character_prompts.append({
"frame": char_frame,
"check_var": self.char_check_vars[i],
"prompt_widget": prompt_text,
"uc_widget": uc_text
})
# 3. '๊ณ„์ •์„ค์ •' ํƒญ ๋‚ด์šฉ ๊ตฌ์„ฑ
token_frame = ttk.LabelFrame(account_settings_tab, text="Novel AI ์˜๊ตฌ ํ† ํฐ ์ž…๋ ฅ")
token_frame.pack(fill="x", expand=False, padx=5, pady=5)
token_frame.grid_columnconfigure(0, weight=1)
token_entry = ttk.Entry(token_frame, textvariable=self.nai_token_var, show="*")
token_entry.grid(row=0, column=0, sticky="ew", padx=(5,5), pady=5)
verify_btn = ttk.Button(token_frame, text="๊ฒ€์ฆ", command=self.verify_token)
verify_btn.grid(row=0, column=1, sticky="e", padx=(0,5), pady=5)
model_frame = ttk.LabelFrame(account_settings_tab, text="๋ชจ๋ธ ์„ค์ •")
model_frame.pack(fill="x", expand=False, padx=5, pady=(10, 5))
model_frame.grid_columnconfigure(0, weight=1)
model_list = ["nai-diffusion-4-5-full"]
model_cb = ttk.Combobox(model_frame, values=model_list, textvariable=self.model_var, state="disabled")
model_cb.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
model_cb.set(model_list[0])
# --- UI ์ดˆ๊ธฐ ์ƒํƒœ ์„ค์ • ---
self.show_generation_preview()
self.update_character_layout()
self.toggle_expand_ui()
def update_character_layout(self):
for p_info in self.character_prompts:
if p_info['check_var'].get():
# ์ฒดํฌ๋œ ๊ฒฝ์šฐ: ์ž…๋ ฅ์ฐฝ ํ™œ์„ฑํ™”
p_info['prompt_widget'].config(state=tk.NORMAL)
p_info['uc_widget'].config(state=tk.NORMAL)
else:
# ์ฒดํฌ ํ•ด์ œ๋œ ๊ฒฝ์šฐ: ์ž…๋ ฅ์ฐฝ ๋น„ํ™œ์„ฑํ™”
p_info['prompt_widget'].config(state=tk.DISABLED)
p_info['uc_widget'].config(state=tk.DISABLED)
def verify_token(self):
"""[๊ฒ€์ฆ] ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ํ† ํฐ ๊ฒ€์ฆ ์Šค๋ ˆ๋“œ๋ฅผ ์‹œ์ž‘ํ•˜๋Š” ๋ฉ”์„œ๋“œ"""
token = self.nai_token_var.get()
if not token:
messagebox.showwarning("์ž…๋ ฅ ์˜ค๋ฅ˜", "ํ† ํฐ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
return
# ๊ฒ€์ฆ ์‹œ์ž‘์„ ์•Œ๋ฆผ (์„ ํƒ ์‚ฌํ•ญ)
self.root.config(cursor="watch")
# ๋ณ„๋„ ์Šค๋ ˆ๋“œ์—์„œ API ์š”์ฒญ ์‹คํ–‰
thread = threading.Thread(target=self._verify_token_thread, args=(token,))
thread.daemon = True
thread.start()
def _verify_token_thread(self, token):
"""(์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰) ์‹ค์ œ API ์š”์ฒญ ๋ฐ ๊ฒ€์ฆ ๋กœ์ง"""
import requests
try:
response = requests.get(
"https://api.novelai.net/user/subscription",
headers={"Authorization": f"Bearer {token}"},
timeout=5 # 5์ดˆ ์ด์ƒ ์‘๋‹ต ์—†์œผ๋ฉด ํƒ€์ž„์•„์›ƒ
)
response.raise_for_status() # 200๋ฒˆ๋Œ€ ์ฝ”๋“œ๊ฐ€ ์•„๋‹ˆ๋ฉด ์˜ˆ์™ธ ๋ฐœ์ƒ
data_dict = response.json()
# Opus ๋“ฑ๊ธ‰(unlimitedMaxPriority) ๊ตฌ๋… ์—ฌ๋ถ€ ํ™•์ธ
if data_dict.get('perks', {}).get('unlimitedMaxPriority', False):
result_message = "Opus ๋“ฑ๊ธ‰ ๊ตฌ๋…์ด ํ™•์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."
result_type = "info"
else:
result_message = "์œ ํšจํ•œ ํ† ํฐ์ด๋‚˜ Opus ๋“ฑ๊ธ‰ ๊ตฌ๋…์ด ์•„๋‹™๋‹ˆ๋‹ค."
result_type = "warning"
except requests.exceptions.HTTPError as e:
if e.response.status_code == 401:
result_message = "์ธ์ฆ ์‹คํŒจ: ์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค."
else:
result_message = f"HTTP ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e.response.status_code}"
result_type = "error"
except requests.exceptions.RequestException as e:
result_message = f"๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜: API ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.\n{e}"
result_type = "error"
except Exception as e:
result_message = f"์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}"
result_type = "error"
# GUI ์—…๋ฐ์ดํŠธ๋Š” ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ
self.root.after(0, self._update_verification_result, result_type, result_message)
def _update_verification_result(self, result_type, message):
"""(๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰) ๊ฒ€์ฆ ๊ฒฐ๊ณผ๋ฅผ messagebox๋กœ ํ‘œ์‹œ"""
self.root.config(cursor="") # ์ปค์„œ ์›๋ž˜๋Œ€๋กœ
if result_type == "info":
messagebox.showinfo("๊ฒ€์ฆ ์„ฑ๊ณต", message)
elif result_type == "warning":
messagebox.showwarning("๊ฒ€์ฆ ํ™•์ธ", message)
else: # error
messagebox.showerror("๊ฒ€์ฆ ์‹คํŒจ", message)
def init_filter_data(self):
try:
from tagbag import bag_of_tags, clothes_list
from character_dictionary import character_dictionary as cd
self.bag_of_tags = bag_of_tags
self.clothes_list = clothes_list
self.character_keys = list(cd.keys()) if isinstance(cd, dict) else []
except ImportError as e:
print(f"โš  NAIA ํŒŒ์ผ import ์‹คํŒจ: {e}")
print("๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.")
self.bag_of_tags = [
'smile', 'blush', 'open_mouth', 'closed_eyes', 'wink', 'frown',
'twintails', 'ponytail', 'braid', 'long_hair', 'short_hair',
'large_breasts', 'small_breasts', 'cleavage', 'navel'
]
self.clothes_list = [
'dress', 'skirt', 'shirt', 'jacket', 'uniform', 'school_uniform'
]
self.character_keys = []
self.colors = ['black', 'white', 'blond', 'silver', 'gray', 'yellow',
'blue', 'purple', 'red', 'pink', 'brown', 'orange',
'green', 'aqua', 'gradient']
def toggle_removed_tags(self):
if self.show_removed_tags_var.get():
self.removed_tags_text.grid(row=3, column=0, sticky="nsew", pady=(5, 0))
else:
self.removed_tags_text.grid_forget()
def toggle_ignore_tags(self):
if self.show_ignore_tags_var.get():
self.ignore_tags_frame.grid(row=1, column=0, sticky="nsew")
else:
self.ignore_tags_frame.grid_forget()
def toggle_prompts(self):
if self.prompt_active_var.get():
self.prompt_notebook.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(5, 0))
else:
self.prompt_notebook.grid_forget()
def on_closing(self):
self.save_settings()
self.root.destroy()
def save_settings(self):
"""ํ˜„์žฌ UI์˜ ๋ชจ๋“  ์„ค์ •์„ JSON ํŒŒ์ผ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค."""
settings = {
# ์ขŒ์ธก ํŒจ๋„ ์„ค์ •
'general_threshold': self.general_threshold_var.get(),
'char_threshold': self.char_threshold_var.get(),
'rm_c': self.rm_c_var.get(),
'rm_color': self.rm_color_var.get(),
'rm_clothes': self.rm_clothes_var.get(),
'webui_mode': self.webui_mode_var.get(),
'instant_inference': self.instant_inference_var.get(),
'ignore_tags': self.ignore_tags_text.get(1.0, tk.END).strip(),
'show_removed_tags': self.show_removed_tags_var.get(),
'show_ignore_tags': self.show_ignore_tags_var.get(),
'prompt_active': self.prompt_active_var.get(),
'prefix_prompt': self.prefix_text.get(1.0, tk.END).strip(),
'suffix_prompt': self.suffix_text.get(1.0, tk.END).strip(),
# ์šฐ์ธก ํŒจ๋„ (์ƒ์„ฑ UI) ์„ค์ •
'expand_ui': self.expand_ui_var.get(),
'sampler': self.sampler_var.get(),
'resolution': self.resolution_var.get(),
'steps': self.steps_var.get(),
'scale': self.scale_var.get(),
'seed': self.seed_var.get(),
'cfg_rescale': self.cfg_rescale_var.get(),
'uc_prompt': self.uc_text.get(1.0, tk.END).strip(),
'nai_token': self.nai_token_var.get()
}
settings['auto_save'] = self.auto_save_var.get()
settings['auto_rating_tag'] = self.auto_rating_tag_var.get()
# ์บ๋ฆญํ„ฐ ํ”„๋กฌํ”„ํŠธ ์„ค์ • ์ €์žฅ
char_prompts_data = []
for p_info in self.character_prompts:
char_prompts_data.append({
"checked": p_info['check_var'].get(),
"prompt": p_info['prompt_widget'].get(1.0, tk.END).strip(),
"uc": p_info['uc_widget'].get(1.0, tk.END).strip()
})
settings['character_prompts'] = char_prompts_data
try:
with open(self.SETTINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(settings, f, indent=4)
except Exception as e:
messagebox.showerror("์ €์žฅ ์‹คํŒจ", f"์„ค์ • ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {e}")
def load_settings(self):
"""JSON ํŒŒ์ผ์—์„œ ์„ค์ •์„ ๋ถˆ๋Ÿฌ์™€ UI์— ์ ์šฉํ•ฉ๋‹ˆ๋‹ค."""
try:
if os.path.exists(self.SETTINGS_FILE):
with open(self.SETTINGS_FILE, 'r', encoding='utf-8') as f:
settings = json.load(f)
# ์ขŒ์ธก ํŒจ๋„ ์„ค์ • ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
self.general_threshold_var.set(settings.get('general_threshold', 0.5))
self.char_threshold_var.set(settings.get('char_threshold', 0.85))
self.rm_c_var.set(settings.get('rm_c', False))
self.rm_color_var.set(settings.get('rm_color', False))
self.rm_clothes_var.set(settings.get('rm_clothes', False))
self.webui_mode_var.set(settings.get('webui_mode', False))
self.instant_inference_var.set(settings.get('instant_inference', False))
self.ignore_tags_text.delete(1.0, tk.END)
self.ignore_tags_text.insert(1.0, settings.get('ignore_tags', ''))
self.show_removed_tags_var.set(settings.get('show_removed_tags', True))
self.show_ignore_tags_var.set(settings.get('show_ignore_tags', False))
self.prompt_active_var.set(settings.get('prompt_active', False))
self.prefix_text.delete(1.0, tk.END)
self.prefix_text.insert(1.0, settings.get('prefix_prompt', ''))
self.suffix_text.delete(1.0, tk.END)
self.suffix_text.insert(1.0, settings.get('suffix_prompt', ''))
# ์šฐ์ธก ํŒจ๋„ (์ƒ์„ฑ UI) ์„ค์ • ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
self.expand_ui_var.set(settings.get('expand_ui', False))
self.sampler_var.set(settings.get('sampler', 'k_euler_ancestral + karras'))
self.resolution_var.set(settings.get('resolution', '896 x 1152'))
self.steps_var.set(settings.get('steps', '28'))
self.scale_var.set(settings.get('scale', '5.8'))
self.seed_var.set(settings.get('seed', '-1'))
self.cfg_rescale_var.set(settings.get('cfg_rescale', '0.25'))
self.uc_text.delete(1.0, tk.END)
self.uc_text.insert(1.0, settings.get('uc_prompt', ''))
self.nai_token_var.set(settings.get('nai_token', ''))
self.auto_save_var.set(settings.get('auto_save', True))
self.auto_rating_tag_var.set(settings.get('auto_rating_tag', True))
# ์บ๋ฆญํ„ฐ ํ”„๋กฌํ”„ํŠธ ์„ค์ • ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
char_prompts_data = settings.get('character_prompts', [])
for i, data in enumerate(char_prompts_data):
if i < len(self.character_prompts):
p_info = self.character_prompts[i]
p_info['check_var'].set(data.get('checked', i==0)) # ๊ธฐ๋ณธ๊ฐ’์€ ์ฒซ๋ฒˆ์งธ๋งŒ True
p_info['prompt_widget'].delete(1.0, tk.END)
p_info['prompt_widget'].insert(1.0, data.get('prompt', ''))
p_info['uc_widget'].delete(1.0, tk.END)
p_info['uc_widget'].insert(1.0, data.get('uc', ''))
# UI ์ƒํƒœ ์—…๋ฐ์ดํŠธ
self.toggle_removed_tags()
self.toggle_ignore_tags()
self.toggle_prompts()
self.update_character_layout()
self.toggle_expand_ui() # ์ €์žฅ๋œ ๊ฐ’์— ๋”ฐ๋ผ ํ™•์žฅ UI ์ƒํƒœ ๊ฒฐ์ •
except Exception as e:
messagebox.showwarning("์„ค์ • ๋กœ๋“œ ์‹คํŒจ", f"์„ค์ • ํŒŒ์ผ({self.SETTINGS_FILE})์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค: {e}")
def handle_paste(self, event):
try:
clipboard_image = ImageGrab.grabclipboard()
if isinstance(clipboard_image, Image.Image):
self.load_from_clipboard()
return "break"
except (ValueError, TypeError):
pass
except Exception as e:
print(f"ํด๋ฆฝ๋ณด๋“œ ์ด๋ฏธ์ง€ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜: {e}")
try:
clipboard_text = self.root.clipboard_get()
if isinstance(clipboard_text, str) and clipboard_text.startswith(('http://', 'https://')):
self._load_image_from_url(clipboard_text)
return "break"
image_extensions = ('.jpg', '.jpeg', '.png', '.webp', '.bmp', '.tiff')
if isinstance(clipboard_text, str) and clipboard_text.lower().endswith(image_extensions):
path = clipboard_text.strip('"')
if os.path.exists(path):
self._load_image_from_path(path)
return "break"
except tk.TclError:
pass
except Exception as e:
messagebox.showerror("๋ถ™์—ฌ๋„ฃ๊ธฐ ์˜ค๋ฅ˜", f"์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
return "break"
return
def _load_image_from_url(self, url):
try:
img = self.download_image_from_url(url)
temp_dir = tempfile.gettempdir()
temp_file = os.path.join(temp_dir, f"wd14_url_{os.getpid()}.png")
img.save(temp_file)
self.current_image_path = temp_file
self.show_preview(temp_file)
if self.session is not None:
self.extract_btn.config(state='normal')
if self.instant_inference_var.get():
self.extract_tags()
except Exception as e:
messagebox.showerror("์˜ค๋ฅ˜", f"์›น ์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ: {str(e)}")
def load_model(self):
try:
import onnxruntime as ort
model_path = resource_path("model.onnx")
tags_path = resource_path("selected_tags.csv")
self.session = ort.InferenceSession(model_path, providers=['CPUExecutionProvider'])
self.tags_df = pd.read_csv(tags_path)
except Exception as e:
messagebox.showerror("์˜ค๋ฅ˜", f"๋ชจ๋ธ ๋กœ๋“œ ์‹คํŒจ: {str(e)}")
def show_initial_preview(self):
try:
white_image = Image.new('RGB', (290, 290), '#F0F0F0')
photo = ImageTk.PhotoImage(white_image)
self.image_label.config(image=photo, text="์ด๋ฏธ์ง€๋ฅผ Drag & Drop\n๋˜๋Š” ๋ถ™์—ฌ๋„ฃ๊ธฐ(Ctrl+V) ํ•ด์ฃผ์„ธ์š”", compound="center", foreground="gray")
self.image_label.image = photo
except Exception as e:
self.image_label.config(text="์ด๋ฏธ์ง€๋ฅผ Drag & Drop ํ•ด์ฃผ์„ธ์š”")
def setup_drag_drop(self):
try:
from tkinterdnd2 import TkinterDnD, DND_ALL
self.TkdndVersion = TkinterDnD._require(self.root)
self.image_label.drop_target_register(DND_ALL)
self.image_label.dnd_bind('<<Drop>>', self.on_drop)
self.image_label.dnd_bind('<<DragEnter>>', self.on_drag_enter)
self.image_label.dnd_bind('<<DragLeave>>', self.on_drag_leave)
except ImportError:
print("tkinterdnd2๊ฐ€ ์„ค์น˜๋˜์ง€ ์•Š์Œ. pip install tkinterdnd2๋กœ ์„ค์น˜ํ•˜๋ฉด ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.")
except Exception as e:
print(f"Drag & Drop ์ดˆ๊ธฐํ™” ์˜ค๋ฅ˜: {e}")
def on_drag_enter(self, event):
try:
self.preview_frame.config(text="๋ฏธ๋ฆฌ๋ณด๊ธฐ (์ด๋ฏธ์ง€๋ฅผ ๋†“์•„์ฃผ์„ธ์š”!)")
except: pass
def on_drag_leave(self, event):
try:
self.preview_frame.config(text="๋ฏธ๋ฆฌ๋ณด๊ธฐ")
except: pass
def download_image_from_url(self, url):
import requests
response = requests.get(url)
response.raise_for_status()
return Image.open(io.BytesIO(response.content))
def on_drop(self, event):
try:
if not isinstance(event, str):
file_path_or_url = event.data.strip("{}")
else:
file_path_or_url = event
if not file_path_or_url:
messagebox.showwarning("๊ฒฝ๊ณ ", "๋“œ๋กญ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
self.preview_frame.config(text="๋ฏธ๋ฆฌ๋ณด๊ธฐ")
return
if file_path_or_url.startswith(('http://', 'https://')):
self._load_image_from_url(file_path_or_url)
self.preview_frame.config(text="๋ฏธ๋ฆฌ๋ณด๊ธฐ")
return
else:
if file_path_or_url.startswith(('blob:')):
messagebox.showwarning("Blob URL ์˜ค๋ฅ˜", "NAI ํ™ˆํŽ˜์ด์ง€์—์„œ ์ƒ์„ฑํ•œ ์ด๋ฏธ์ง€๋Š” ์ฆ‰์‹œ ๋ณต์‚ฌํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.\n์ด๋ฏธ์ง€๋ฅผ ๋‹ค์šด๋กœ๋“œํ•œ ํ›„ ๋“œ๋ž˜๊ทธ & ๋“œ๋กญํ•ด์ฃผ์„ธ์š”.")
self.preview_frame.config(text="๋ฏธ๋ฆฌ๋ณด๊ธฐ")
return
file_path_or_url = file_path_or_url.replace("\\", '/')
if os.path.exists(file_path_or_url):
self.current_image_path = file_path_or_url
self.show_preview(file_path_or_url)
if self.session is not None:
self.extract_btn.config(state='normal')
if self.instant_inference_var.get():
self.extract_tags()
self.preview_frame.config(text="๋ฏธ๋ฆฌ๋ณด๊ธฐ")
else:
messagebox.showerror("์˜ค๋ฅ˜", f"์œ ํšจํ•˜์ง€ ์•Š์€ ํŒŒ์ผ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค: '{file_path_or_url}'")
self.preview_frame.config(text="๋ฏธ๋ฆฌ๋ณด๊ธฐ")
except Exception as e:
messagebox.showerror("์˜ค๋ฅ˜", f"ํŒŒ์ผ ๋“œ๋กญ ์˜ค๋ฅ˜: {str(e)}")
self.preview_frame.config(text="๋ฏธ๋ฆฌ๋ณด๊ธฐ")
def _load_image_from_path(self, path):
if os.path.exists(path):
self.current_image_path = path
self.show_preview(path)
if self.session is not None:
self.extract_btn.config(state='normal')
if self.instant_inference_var.get():
self.extract_tags()
else:
messagebox.showwarning("๊ฒฝ๋กœ ์˜ค๋ฅ˜", f"ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {path}")
def load_from_clipboard(self):
try:
image = ImageGrab.grabclipboard()
if isinstance(image, Image.Image):
temp_dir = tempfile.gettempdir()
temp_file = os.path.join(temp_dir, f"wd14_clipboard_{os.getpid()}.png")
if image.mode == 'RGBA':
image = image.convert('RGB')
image.save(temp_file, 'PNG')
self.current_image_path = temp_file
self.show_preview(temp_file)
if self.session is not None:
self.extract_btn.config(state='normal')
if self.instant_inference_var.get():
self.extract_tags()
else:
messagebox.showinfo("์ •๋ณด", "ํด๋ฆฝ๋ณด๋“œ์— ์ด๋ฏธ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
except Exception as e:
messagebox.showerror("์˜ค๋ฅ˜", f"ํด๋ฆฝ๋ณด๋“œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์˜ค๋ฅ˜: {str(e)}")
def load_image(self):
file_path = filedialog.askopenfilename(
title="์ด๋ฏธ์ง€ ์„ ํƒ",
filetypes=[("์ด๋ฏธ์ง€ ํŒŒ์ผ", "*.jpg *.jpeg *.png *.bmp *.tiff *.webp")]
)
if file_path:
self.current_image_path = file_path
self.show_preview(file_path)
if self.session is not None:
self.extract_btn.config(state='normal')
if self.instant_inference_var.get():
self.extract_tags()
def show_preview(self, image_path):
try:
image = Image.open(image_path).convert('RGB')
size = max(image.size)
square_image = Image.new('RGB', (size, size), 'white')
paste_x = (size - image.width) // 2
paste_y = (size - image.height) // 2
square_image.paste(image, (paste_x, paste_y))
square_image.thumbnail((290, 290), Image.Resampling.LANCZOS)
photo = ImageTk.PhotoImage(square_image)
self.image_label.config(image=photo, text="")
self.image_label.image = photo
except Exception as e:
self.image_label.config(text=f"์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ: {str(e)}")
def extract_tags(self):
if not self.current_image_path or not self.session:
return
self.extract_btn.config(state='disabled', text='์ถ”์ถœ ์ค‘...')
self.result_text.delete(1.0, tk.END)
self.result_text.insert(tk.END, "ํƒœ๊ทธ ์ถ”์ถœ ์ค‘...")
self.removed_tags_text.config(state=tk.NORMAL)
self.removed_tags_text.delete(1.0, tk.END)
self.removed_tags_text.config(state=tk.DISABLED)
thread = threading.Thread(target=self._extract_tags_thread)
thread.daemon = True
thread.start()
def _extract_tags_thread(self):
try:
input_image = Image.open(self.current_image_path)
_, height, _, _ = self.session.get_inputs()[0].shape
image = input_image.convert('RGBA')
new_image = Image.new('RGBA', image.size, 'WHITE')
new_image.paste(image, mask=image)
image = new_image.convert('RGB')
image = np.asarray(image)[:, :, ::-1]
image = self.make_square(image, height)
image = self.smart_resize(image, height)
image = image.astype(np.float32)
image = np.expand_dims(image, 0)
input_name = self.session.get_inputs()[0].name
label_name = self.session.get_outputs()[0].name
confidents = self.session.run([label_name], {input_name: image})[0]
tags_with_conf = []
for i, conf in enumerate(confidents[0]):
if i < len(self.tags_df):
tags_with_conf.append((self.tags_df.iloc[i]['name'], float(conf)))
rating_tags = dict(tags_with_conf[:4])
general_tags = dict(tags_with_conf[4:])
best_rating = max(rating_tags, key=rating_tags.get) if rating_tags else None
result_tuple = self._format_results(general_tags, best_rating) # best_rating ์ „๋‹ฌ
self.root.after(0, self._update_results, result_tuple)
except Exception as e:
error_msg = f"ํƒœ๊ทธ ์ถ”์ถœ ์˜ค๋ฅ˜: {str(e)}"
self.root.after(0, self._update_results, ((None, error_msg, "")))
def apply_tag_filters(self, tags_list, best_rating=None):
original_tags = tags_list.copy()
tags_to_process = [tag.replace('_', ' ') for tag in original_tags]
final_indices = list(range(len(tags_to_process)))
user_removed_originals = []
other_removed_originals = []
ignore_list_str = self.ignore_tags_text.get(1.0, tk.END).strip()
if ignore_list_str:
user_ignored_tags = {tag.strip().replace('_', ' ') for tag in ignore_list_str.split(',') if tag.strip()}
indices_to_remove = {i for i in final_indices if tags_to_process[i] in user_ignored_tags}
for i in indices_to_remove:
user_removed_originals.append(original_tags[i])
final_indices = [i for i in final_indices if i not in indices_to_remove]
if self.rm_c_var.get():
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])}
for i in indices_to_remove:
other_removed_originals.append(original_tags[i])
final_indices = [i for i in final_indices if i not in indices_to_remove]
if self.rm_color_var.get():
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]}
for i in indices_to_remove:
other_removed_originals.append(original_tags[i])
final_indices = [i for i in final_indices if i not in indices_to_remove]
if self.rm_clothes_var.get():
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}
for i in indices_to_remove:
other_removed_originals.append(original_tags[i])
final_indices = [i for i in final_indices if i not in indices_to_remove]
final_tags_with_space = [tags_to_process[i] for i in final_indices]
if self.auto_rating_tag_var.get() and best_rating:
prefix_str = self.prefix_text.get(1.0, tk.END)
suffix_str = self.suffix_text.get(1.0, tk.END)
if not self.webui_mode_var.get():
best_rating = "rating:" + best_rating
tag_to_add = None
if best_rating in ["rating:explicit", "rating:questionable", "questionable", "explicit"]:
if "nsfw" not in prefix_str and "nsfw" not in suffix_str:
tag_to_add = best_rating +", nsfw"
elif best_rating in ["rating:general", "general"]:
if "safe" not in prefix_str and "safe" not in suffix_str:
tag_to_add = best_rating +", safe"
else:
tag_to_add = best_rating
# nsfw/safe ํƒœ๊ทธ ์ถ”๊ฐ€
if tag_to_add:
person_tags = {'1girl', '2girls', '3girls', '4girls', '5girls', '6+girls',
'1boy', '2boys', '3boys', '4boys', '5boys', '6+boys'}
insert_pos = 0
for i, tag in enumerate(final_tags_with_space):
if tag in person_tags:
insert_pos = i + 1
break
final_tags_with_space.insert(insert_pos, tag_to_add)
if self.webui_mode_var.get():
final_tags_formatted = [tag.replace('(', '\\(').replace(')', '\\)') for tag in final_tags_with_space]
else:
final_tags_formatted = final_tags_with_space
final_tags_with_type = [(tag, 'original') for tag in final_tags_formatted]
if self.prompt_active_var.get():
prefix_str = self.prefix_text.get(1.0, tk.END).strip()
if prefix_str:
prefix_tags = [t.strip() for t in prefix_str.split(',') if t.strip()]
prefix_tags_with_type = [(tag, 'prompt') for tag in prefix_tags]
person_tags = {'1girl', '2girls', '3girls', '4girls', '5girls', '6+girls',
'1boy', '2boys', '3boys', '4boys', '5boys', '6+boys'}
last_person_tag_index = -1
search_range = min(len(final_tags_with_type), 4)
for i in range(search_range):
if final_tags_with_type[i][0] in person_tags:
last_person_tag_index = i
if last_person_tag_index != -1:
final_tags_with_type[last_person_tag_index+1:last_person_tag_index+1] = prefix_tags_with_type
else:
final_tags_with_type[0:0] = prefix_tags_with_type
suffix_str = self.suffix_text.get(1.0, tk.END).strip()
if suffix_str:
suffix_tags = [t.strip() for t in suffix_str.split(',') if t.strip()]
suffix_tags_with_type = [(tag, 'prompt') for tag in suffix_tags]
final_tags_with_type.extend(suffix_tags_with_type)
return final_tags_with_type, user_removed_originals, list(set(other_removed_originals))
def _is_character_name(self, tag):
if hasattr(self, 'character_keys') and tag in self.character_keys:
return True
if ('_' in tag and any(c.isupper() for c in tag) and
not tag.startswith('1') and
not any(word in tag.lower() for word in ['girl', 'boy', 'solo', 'multiple']) and
not any(word in tag.lower() for word in ['hair', 'eye', 'dress', 'shirt']) and
len(tag) > 3):
return True
return False
def _format_results(self, tags, best_rating=None):
general_threshold = self.general_threshold_var.get()
char_threshold = self.char_threshold_var.get()
person_tags = {'1girl', '2girls', '3girls', '4girls', '5girls', '6+girls',
'1boy', '2boys', '3boys', '4boys', '5boys', '6+boys',
'multiple_girls', 'multiple_boys', 'solo', 'no_humans'}
filtered_tags = []
for tag, conf in tags.items():
if tag in person_tags:
if conf >= 0.9: filtered_tags.append(tag)
elif any(c.isupper() for c in tag) and '_' in tag and conf >= char_threshold:
filtered_tags.append(tag)
elif conf >= general_threshold:
filtered_tags.append(tag)
if len(filtered_tags) > 99:
filtered_tags = filtered_tags[:99]
final_tags_with_type, user_removed, other_removed = self.apply_tag_filters(filtered_tags, best_rating)
removed_for_display = []
for tag in user_removed:
removed_for_display.append((tag, 'ignored'))
for tag in other_removed:
if tag not in user_removed:
removed_for_display.append((tag, 'other'))
return final_tags_with_type, removed_for_display
def copy_all_tags(self):
content = self.result_text.get(1.0, tk.END).strip()
if content:
self.root.clipboard_clear()
self.root.clipboard_append(content)
else:
messagebox.showwarning("๋ณต์‚ฌ ์‹คํŒจ", "๋ณต์‚ฌํ•  ํƒœ๊ทธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
def copy_from_second(self):
self._copy_from_index(1, "2๋ฒˆ ํƒœ๊ทธ๋ถ€ํ„ฐ")
def copy_from_third(self):
self._copy_from_index(2, "3๋ฒˆ ํƒœ๊ทธ๋ถ€ํ„ฐ")
def _copy_from_index(self, start_index, description):
content = self.result_text.get(1.0, tk.END).strip()
if content:
tags_list = [tag.strip() for tag in content.split(',')]
if len(tags_list) > start_index:
selected_tags = tags_list[start_index:]
result = ', '.join(selected_tags)
self.root.clipboard_clear()
self.root.clipboard_append(result)
else:
messagebox.showwarning("๋ณต์‚ฌ ์‹คํŒจ", f"ํƒœ๊ทธ๊ฐ€ {start_index + 1}๊ฐœ ๋ฏธ๋งŒ์ž…๋‹ˆ๋‹ค.")
else:
messagebox.showwarning("๋ณต์‚ฌ ์‹คํŒจ", "๋ณต์‚ฌํ•  ํƒœ๊ทธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
def _update_results(self, result_tuple):
main_tags_info, removed_tags_info = result_tuple[0], result_tuple[1]
if len(result_tuple) == 3: # ์—๋Ÿฌ๊ฐ€ ์•„๋‹Œ ์ •์ƒ ๊ฒฐ๊ณผ์ผ ๊ฒฝ์šฐ
main_tags_info, removed_tags_info = result_tuple[1], result_tuple[2]
# ๋ฉ”์ธ ํƒœ๊ทธ ๋ฐ•์Šค ์—…๋ฐ์ดํŠธ
self.result_text.delete(1.0, tk.END)
if isinstance(main_tags_info, list):
for i, (tag, tag_type) in enumerate(main_tags_info):
if i > 0:
self.result_text.insert(tk.END, ', ')
if tag_type == 'prompt':
self.result_text.insert(tk.END, tag, 'prompt')
else:
self.result_text.insert(tk.END, tag)
else: # ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ
self.result_text.insert(tk.END, main_tags_info)
self.removed_tags_text.config(state=tk.NORMAL)
self.removed_tags_text.delete(1.0, tk.END)
if removed_tags_info:
for i, (tag, tag_type) in enumerate(removed_tags_info):
display_tag = tag.replace('_', ' ')
if i > 0:
self.removed_tags_text.insert(tk.END, ', ')
if tag_type == 'ignored':
self.removed_tags_text.insert(tk.END, display_tag, 'ignored')
else:
self.removed_tags_text.insert(tk.END, display_tag)
self.removed_tags_text.config(state=tk.DISABLED)
self.extract_btn.config(state='normal', text='ํƒœ๊ทธ ์ถ”์ถœ')
if (self.instant_generate_var.get() and
self.expand_ui_var.get() and
self.is_generating == False):
self.start_generation()
def make_square(self, img, target_size):
old_size = img.shape[:2]
desired_size = max(old_size)
desired_size = max(desired_size, target_size)
delta_w = desired_size - old_size[1]
delta_h = desired_size - old_size[0]
top, bottom = delta_h // 2, delta_h - (delta_h // 2)
left, right = delta_w // 2, delta_w - (delta_w // 2)
color = [255, 255, 255]
new_im = cv2.copyMakeBorder(
img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color
)
return new_im
def smart_resize(self, img, size):
if img.shape[0] > size:
img = cv2.resize(img, (size, size), interpolation=cv2.INTER_AREA)
elif img.shape[0] < size:
img = cv2.resize(img, (size, size), interpolation=cv2.INTER_CUBIC)
return img
def find_max_resolution(self, width, height, max_pixels=1048576, multiple_of=64):
"""๊ฐ€๋กœ์„ธ๋กœ ๋น„์œจ์„ ์œ ์ง€ํ•˜๋ฉฐ ์ตœ๋Œ€ ํ”ฝ์…€ ์ˆ˜์— ๋งž๋Š” ์ตœ๋Œ€ ํ•ด์ƒ๋„๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค."""
ratio = width / height
max_width = int((max_pixels * ratio)**0.5)
max_height = int((max_pixels / ratio)**0.5)
max_width = (max_width // multiple_of) * multiple_of
max_height = (max_height // multiple_of) * multiple_of
while max_width * max_height > max_pixels:
max_width -= multiple_of
max_height = int(max_width / ratio)
max_height = (max_height // multiple_of) * multiple_of
return (max_width, max_height)
def start_generation(self):
if not self.nai_token_var.get():
messagebox.showerror("ํ† ํฐ ์˜ค๋ฅ˜", "NAI ์„ค์ • ํƒญ์—์„œ API ํ† ํฐ์„ ๋จผ์ € ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
return
if not self.result_text.get(1.0, tk.END).strip():
messagebox.showerror("ํ”„๋กฌํ”„ํŠธ ์˜ค๋ฅ˜", "์ขŒ์ธก '์ถ”์ถœ๋œ ํƒœ๊ทธ'์— ํ”„๋กฌํ”„ํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
return
# --- ์ˆ˜์ •๋œ ๋ถ€๋ถ„ ---
self.is_generating = True
self.generation_start_time = time.time()
self.generation_btn.config(state=tk.DISABLED)
self.save_btn.config(state=tk.NORMAL)
self.generation_image_label.config(image=None, text="๐ŸŽจ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ค‘...")
self._update_timer() # ํƒ€์ด๋จธ ์‹œ์ž‘
thread = threading.Thread(target=self._generation_thread)
thread.daemon = True
thread.start()
def _generation_thread(self):
"""(๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ) ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆ˜์ง‘, API ์š”์ฒญ ๋ฐ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜"""
try:
# 1. ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆ˜์ง‘ ๋ฐ ์ „์ฒ˜๋ฆฌ
access_token = self.nai_token_var.get()
model_name = self.model_var.get()
main_prompt = self.result_text.get(1.0, tk.END).strip()
negative_prompt = self.uc_text.get(1.0, tk.END).strip()
# ํ•ด์ƒ๋„ ์ฒ˜๋ฆฌ
resolution_str = self.resolution_var.get()
if resolution_str == "์ž๋™ ํ•ด์ƒ๋„":
if self.current_image_path and os.path.exists(self.current_image_path):
with Image.open(self.current_image_path) as img:
w, h = img.size
width, height = self.find_max_resolution(w, h)
else: # ๋ถˆ๋Ÿฌ์˜จ ์ด๋ฏธ์ง€๊ฐ€ ์—†์œผ๋ฉด 1:1 ๋น„์œจ๋กœ ๊ณ„์‚ฐ
width, height = self.find_max_resolution(1024, 1024)
else:
parts = resolution_str.split(' x ')
width, height = int(parts[0]), int(parts[1])
# ์ƒ˜ํ”Œ๋Ÿฌ ๋ฐ ์Šค์ผ€์ค„๋Ÿฌ ๋ถ„๋ฆฌ
sampler_parts = self.sampler_var.get().split(' + ')
sampler = sampler_parts[0]
noise_schedule = sampler_parts[1] if len(sampler_parts) > 1 else 'karras' # ๊ธฐ๋ณธ๊ฐ’
seed = int(self.seed_var.get())
if seed == -1:
seed = random.randint(0, 9999999999)
self.last_generated_seed = seed
# 2. API Payload ๊ตฌ์„ฑ
parameters = {
"width": width,
"height": height,
"n_samples": 1,
"seed": seed,
"extra_noise_seed": seed,
"sampler": sampler,
"steps": int(self.steps_var.get()),
"scale": float(self.scale_var.get()),
"negative_prompt": negative_prompt,
"cfg_rescale": float(self.cfg_rescale_var.get()),
"noise_schedule": noise_schedule,
}
data = {
"input": main_prompt,
"model": model_name,
"action": "generate",
"parameters": parameters
}
# V4 ํŠนํ™” ์„ค์ •
if 'nai-diffusion-4' in model_name:
data['parameters'].update({
'params_version': 3,
'add_original_image': True,
'legacy': False,
'legacy_uc': False,
'autoSmea': True,
'prefer_brownian': True,
'ucPreset': 0,
'use_coords': False,
'v4_negative_prompt': {'caption': {'base_caption': negative_prompt, 'char_captions': []}, 'legacy_uc': False},
'v4_prompt': {'caption': {'base_caption': main_prompt, 'char_captions': []}, 'use_coords': False, 'use_order': True}
})
# ํ™œ์„ฑํ™”๋œ ์บ๋ฆญํ„ฐ ํ”„๋กฌํ”„ํŠธ ์ฒ˜๋ฆฌ
active_chars = [p for p in self.character_prompts if p['check_var'].get()]
if active_chars:
for char_info in active_chars:
char_prompt = char_info['prompt_widget'].get(1.0, tk.END).strip()
char_uc = char_info['uc_widget'].get(1.0, tk.END).strip()
char_v4_prompt = {'char_caption': char_prompt, 'centers': [{'x': 0.5, 'y': 0.5}]}
char_v4_uc = {'char_caption': char_uc, 'centers': [{'x': 0.5, 'y': 0.5}]}
data['parameters']['v4_prompt']['caption']['char_captions'].append(char_v4_prompt)
data['parameters']['v4_negative_prompt']['caption']['char_captions'].append(char_v4_uc)
# 3. API ์š”์ฒญ
import requests
response = requests.post(
"https://image.novelai.net/ai/generate-image",
headers={"Authorization": f"Bearer {access_token}"},
json=data,
timeout=180
)
response.raise_for_status()
# 4. ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ
image = self._process_nai_response(response.content)
self.root.after(0, self._update_generation_result, image)
except Exception as e:
self.root.after(0, self._update_generation_result, e)
def _process_nai_response(self, content):
"""NAI ์‘๋‹ต(zip)์„ ์ฒ˜๋ฆฌํ•˜์—ฌ PIL Image ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค."""
import zipfile
try:
zipped = zipfile.ZipFile(io.BytesIO(content))
image_bytes = zipped.read(zipped.infolist()[0])
image = Image.open(io.BytesIO(image_bytes))
return image
except Exception as e:
raise Exception(f"์‘๋‹ต ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์‹คํŒจ: {e}")
def start_generation(self):
# ... (๊ธฐ์กด ๊ฒ€์ฆ ๋กœ์ง) ...
self.is_generating = True
self.generation_start_time = time.time()
self.generation_btn.config(state=tk.DISABLED)
self.save_btn.config(state=tk.NORMAL) # <<<< ์ƒ์„ฑ ์‹œ์ž‘ ์‹œ ์ €์žฅ ๋ฒ„ํŠผ ํ™œ์„ฑํ™”
self.generation_image_label.config(image=None, text="๐ŸŽจ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ค‘...")
self._update_timer()
thread = threading.Thread(target=self._generation_thread)
thread.daemon = True
thread.start()
def _update_generation_result(self, result):
self.is_generating = False
self.generation_btn.config(state=tk.NORMAL, text="Generation")
if isinstance(result, Image.Image):
self.last_generated_image = result # ์›๋ณธ ์ด๋ฏธ์ง€ ๋ณด๊ด€
self.last_generated_image.save(self.temp_image_path, "PNG") # ์ž„์‹œ ํŒŒ์ผ๋กœ ์ €์žฅ
if self.auto_save_var.get():
try:
save_dir = os.path.join("Output_WD14", self.save_session_folder)
os.makedirs(save_dir, exist_ok=True)
file_path = os.path.join(save_dir, f"{self.save_counter:05d}.png")
self.last_generated_image.save(file_path, "PNG")
self.save_counter += 1
except Exception as e:
print(f"์ž๋™ ์ €์žฅ ์‹คํŒจ: {e}")
thumbnail = self._create_thumbnail(result)
photo = ImageTk.PhotoImage(thumbnail)
self.generation_image_label.config(image=photo, text="")
self.generation_image_label.image = photo
elif isinstance(result, Exception):
self.show_generation_preview()
messagebox.showerror("์ƒ์„ฑ ์‹คํŒจ", f"์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค:\n{result}")
def _show_image_with_pil(self):
"""PIL ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋งˆ์ง€๋ง‰์œผ๋กœ ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€๋ฅผ ๊ธฐ๋ณธ ์ด๋ฏธ์ง€ ๋ทฐ์–ด๋กœ ์—ฝ๋‹ˆ๋‹ค."""
if self.last_generated_image:
try:
self.last_generated_image.show()
except Exception as e:
messagebox.showerror("์˜ค๋ฅ˜", f"์ด๋ฏธ์ง€๋ฅผ ์—ฌ๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {e}")
else:
messagebox.showinfo("์ •๋ณด", "ํ‘œ์‹œํ•  ์ด๋ฏธ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
def main():
try:
from tkinterdnd2 import TkinterDnD
root = TkinterDnD.Tk()
except ImportError:
print("tkinterdnd2๊ฐ€ ์„ค์น˜๋˜์ง€ ์•Š์•„ Drag & Drop ๊ธฐ๋Šฅ์ด ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค.")
root = tk.Tk()
app = WD14TaggerGUI(root)
root.mainloop()
if __name__ == "__main__":
main()