|
| 1 | +import tkinter as tk |
| 2 | +from tkinter import filedialog, ttk, messagebox |
| 3 | +import sys |
| 4 | +import os |
| 5 | +import subprocess |
| 6 | +import shutil |
| 7 | +import winsound |
| 8 | +import tkinter.scrolledtext as scrolledtext |
| 9 | + |
| 10 | +def resource_path(relative_path): |
| 11 | + """ Get absolute path to resource, works for dev and for PyInstaller """ |
| 12 | + try: |
| 13 | + # PyInstaller creates a temp folder and stores path in _MEIPASS |
| 14 | + base_path = sys._MEIPASS |
| 15 | + except Exception: |
| 16 | + base_path = os.path.abspath(".") |
| 17 | + |
| 18 | + return os.path.join(base_path, relative_path) |
| 19 | + |
| 20 | +def scroll_entry_to_end(entry): |
| 21 | + entry.xview_moveto(1) |
| 22 | + |
| 23 | +def browse_file(entry): |
| 24 | + file_path = filedialog.askopenfilename(filetypes=[("Model files", "*.safetensors *.sft")]) |
| 25 | + if file_path: |
| 26 | + entry.delete(0, tk.END) |
| 27 | + entry.insert(0, file_path) |
| 28 | + scroll_entry_to_end(entry) |
| 29 | + update_output_filename() |
| 30 | + |
| 31 | +def browse_output_file(entry): |
| 32 | + file_path = filedialog.asksaveasfilename(defaultextension=".gguf", |
| 33 | + filetypes=[("GGUF files", "*.gguf")]) |
| 34 | + if file_path: |
| 35 | + entry.delete(0, tk.END) |
| 36 | + entry.insert(0, file_path) |
| 37 | + scroll_entry_to_end(entry) |
| 38 | + |
| 39 | +def update_output_filename(*args): |
| 40 | + input_file = input_entry.get() |
| 41 | + quantize_level = quantize_level_var.get() |
| 42 | + if input_file: |
| 43 | + input_dir = os.path.dirname(input_file) |
| 44 | + base_name = os.path.splitext(os.path.basename(input_file))[0] |
| 45 | + output_file = os.path.join(input_dir, f"{base_name}-{quantize_level}.gguf") |
| 46 | + if '/' in input_file: |
| 47 | + output_file = output_file.replace('\\', '/') |
| 48 | + elif '\\' in input_file: |
| 49 | + output_file = output_file.replace('/', '\\') |
| 50 | + output_entry.delete(0, tk.END) |
| 51 | + output_entry.insert(0, output_file) |
| 52 | + scroll_entry_to_end(output_entry) |
| 53 | + |
| 54 | +def disable_ui(): |
| 55 | + input_entry.config(state='disabled') |
| 56 | + output_entry.config(state='disabled') |
| 57 | + input_browse.config(state='disabled') |
| 58 | + output_browse.config(state='disabled') |
| 59 | + quantize_dropdown.config(state='disabled') |
| 60 | + run_button.config(state='disabled') |
| 61 | + |
| 62 | +def enable_ui(): |
| 63 | + input_entry.config(state='normal') |
| 64 | + output_entry.config(state='normal') |
| 65 | + input_browse.config(state='normal') |
| 66 | + output_browse.config(state='normal') |
| 67 | + quantize_dropdown.config(state='readonly') |
| 68 | + run_button.config(state='normal') |
| 69 | + |
| 70 | +def run_llama_quantize(): |
| 71 | + input_file = input_entry.get() |
| 72 | + output_file = output_entry.get() |
| 73 | + quantize_level = quantize_level_var.get() |
| 74 | + |
| 75 | + if not input_file or not output_file: |
| 76 | + messagebox.showerror("Error", "Please select both input and output files.") |
| 77 | + return |
| 78 | + |
| 79 | + output_dir = os.path.dirname(output_file) |
| 80 | + required_space = 40_000_000_000 # ~40 GB (a bit more than 36.5 GB) |
| 81 | + available_space = shutil.disk_usage(output_dir).free |
| 82 | + |
| 83 | + if available_space < required_space: |
| 84 | + required_gb = required_space / (1024**3) |
| 85 | + available_gb = available_space / (1024**3) |
| 86 | + messagebox.showerror("Error", f"You need {required_gb:.1f} GB of drive space to continue. Only {available_gb:.1f} GB available.") |
| 87 | + return |
| 88 | + |
| 89 | + disable_ui() |
| 90 | + |
| 91 | + # Clear previous log |
| 92 | + process_text.delete('1.0', tk.END) |
| 93 | + process_text.insert(tk.END, "Starting conversion process...\n") |
| 94 | + process_text.see(tk.END) |
| 95 | + root.update() |
| 96 | + |
| 97 | + # Convert the input file to GGUF format |
| 98 | + convert_py_path = resource_path("convert.py") |
| 99 | + output_dir = os.path.dirname(output_file) |
| 100 | + temp_gguf_file = os.path.join(output_dir, "temporary_file_during_quantization") |
| 101 | + |
| 102 | + try: |
| 103 | + startupinfo = subprocess.STARTUPINFO() |
| 104 | + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW |
| 105 | + startupinfo.wShowWindow = subprocess.SW_HIDE |
| 106 | + |
| 107 | + process = subprocess.Popen(["python", convert_py_path, "--src", input_file, "--dst", temp_gguf_file], |
| 108 | + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, |
| 109 | + bufsize=1, universal_newlines=True, startupinfo=startupinfo) |
| 110 | + |
| 111 | + for line in process.stdout: |
| 112 | + process_text.insert(tk.END, line) |
| 113 | + process_text.see(tk.END) |
| 114 | + root.update() |
| 115 | + |
| 116 | + process.wait() |
| 117 | + if process.returncode != 0: |
| 118 | + raise subprocess.CalledProcessError(process.returncode, process.args) |
| 119 | + |
| 120 | + process_text.insert(tk.END, "Conversion completed successfully.\n") |
| 121 | + except subprocess.CalledProcessError as e: |
| 122 | + process_text.insert(tk.END, f"Error converting file: {e}\n") |
| 123 | + process_text.insert(tk.END, f"Command: {e.cmd}\n") |
| 124 | + process_text.insert(tk.END, f"Return code: {e.returncode}\n") |
| 125 | + process_text.see(tk.END) |
| 126 | + root.update() |
| 127 | + enable_ui() |
| 128 | + return |
| 129 | + |
| 130 | + # Quantize the converted file |
| 131 | + llama_quantize_path = resource_path("llama-quantize.exe") |
| 132 | + process_text.insert(tk.END, "Starting quantization process...\n") |
| 133 | + process_text.see(tk.END) |
| 134 | + root.update() |
| 135 | + |
| 136 | + try: |
| 137 | + startupinfo = subprocess.STARTUPINFO() |
| 138 | + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW |
| 139 | + startupinfo.wShowWindow = subprocess.SW_HIDE |
| 140 | + |
| 141 | + process = subprocess.Popen([llama_quantize_path, temp_gguf_file, output_file, quantize_level], |
| 142 | + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, |
| 143 | + bufsize=1, universal_newlines=True, startupinfo=startupinfo) |
| 144 | + |
| 145 | + for line in process.stdout: |
| 146 | + process_text.insert(tk.END, line) |
| 147 | + process_text.see(tk.END) |
| 148 | + root.update() |
| 149 | + |
| 150 | + process.wait() |
| 151 | + if process.returncode != 0: |
| 152 | + raise subprocess.CalledProcessError(process.returncode, process.args) |
| 153 | + |
| 154 | + process_text.insert(tk.END, "Quantization completed successfully.\n") |
| 155 | + except subprocess.CalledProcessError as e: |
| 156 | + process_text.insert(tk.END, f"Error running llama-quantize: {e}\n") |
| 157 | + process_text.insert(tk.END, f"Command: {e.cmd}\n") |
| 158 | + process_text.insert(tk.END, f"Return code: {e.returncode}\n") |
| 159 | + process_text.see(tk.END) |
| 160 | + root.update() |
| 161 | + finally: |
| 162 | + # Clean up the temporary file |
| 163 | + if os.path.exists(temp_gguf_file): |
| 164 | + os.remove(temp_gguf_file) |
| 165 | + |
| 166 | + process_text.insert(tk.END, "Quantization process completed.") |
| 167 | + process_text.see(tk.END) |
| 168 | + root.update() |
| 169 | + |
| 170 | + enable_ui() |
| 171 | + |
| 172 | + # Play sound effect |
| 173 | + winsound.PlaySound("SystemAsterisk", winsound.SND_ALIAS) |
| 174 | + |
| 175 | +root = tk.Tk() |
| 176 | +root.title("Easy Quantization GUI") |
| 177 | +root.geometry("800x600") # Enlarge the main window |
| 178 | + |
| 179 | +# Quantize level selection |
| 180 | +quantize_frame = tk.Frame(root) |
| 181 | +quantize_frame.pack(pady=10, padx=10) |
| 182 | + |
| 183 | +quantize_label = tk.Label(quantize_frame, text="Quantize Level:") |
| 184 | +quantize_label.pack(side=tk.LEFT) |
| 185 | + |
| 186 | +quantize_levels = ["Q2_K", "Q3_K_S", "Q4_0", "Q4_1", "Q4_K_S", "Q5_0", "Q5_1", "Q5_K_S", "Q6_K", "Q8_0"] |
| 187 | +quantize_level_var = tk.StringVar(root) |
| 188 | +quantize_level_var.set("Q8_0") # Set default value to Q8_0 |
| 189 | + |
| 190 | +quantize_dropdown = ttk.Combobox(quantize_frame, textvariable=quantize_level_var, values=quantize_levels, state="readonly") |
| 191 | +quantize_dropdown.pack(side=tk.LEFT) |
| 192 | +quantize_dropdown.bind("<<ComboboxSelected>>", lambda event: scroll_entry_to_end(output_entry)) |
| 193 | + |
| 194 | +# Input file selection |
| 195 | +input_frame = tk.Frame(root) |
| 196 | +input_frame.pack(pady=10, padx=10, fill=tk.X) |
| 197 | + |
| 198 | +input_label = tk.Label(input_frame, text="Input File:") |
| 199 | +input_label.pack(side=tk.LEFT) |
| 200 | + |
| 201 | +input_entry = tk.Entry(input_frame) |
| 202 | +input_entry.pack(side=tk.LEFT, expand=True, fill=tk.X) |
| 203 | + |
| 204 | +input_browse = tk.Button(input_frame, text="Browse", command=lambda: browse_file(input_entry)) |
| 205 | +input_browse.pack(side=tk.RIGHT) |
| 206 | + |
| 207 | +# Add binding to scroll input entry when it gains focus |
| 208 | +input_entry.bind("<FocusIn>", lambda event: scroll_entry_to_end(input_entry)) |
| 209 | + |
| 210 | +# Output file selection |
| 211 | +output_frame = tk.Frame(root) |
| 212 | +output_frame.pack(pady=10, padx=10, fill=tk.X) |
| 213 | + |
| 214 | +output_label = tk.Label(output_frame, text="Output File:") |
| 215 | +output_label.pack(side=tk.LEFT) |
| 216 | + |
| 217 | +output_entry = tk.Entry(output_frame) |
| 218 | +output_entry.pack(side=tk.LEFT, expand=True, fill=tk.X) |
| 219 | + |
| 220 | +output_browse = tk.Button(output_frame, text="Browse", command=lambda: browse_output_file(output_entry)) |
| 221 | +output_browse.pack(side=tk.RIGHT) |
| 222 | + |
| 223 | +# Add binding to scroll output entry when it gains focus |
| 224 | +output_entry.bind("<FocusIn>", lambda event: scroll_entry_to_end(output_entry)) |
| 225 | + |
| 226 | +# Run button |
| 227 | +run_button = tk.Button(root, text="Run Quantization", command=run_llama_quantize) |
| 228 | +run_button.pack(pady=20) |
| 229 | + |
| 230 | +# Bind events to update output filename |
| 231 | +input_entry.bind("<KeyRelease>", update_output_filename) |
| 232 | +quantize_level_var.trace_add("write", update_output_filename) |
| 233 | + |
| 234 | +def on_window_resize(event): |
| 235 | + scroll_entry_to_end(input_entry) |
| 236 | + scroll_entry_to_end(output_entry) |
| 237 | + |
| 238 | +root.bind("<Configure>", on_window_resize) |
| 239 | + |
| 240 | +# Add process log to main window |
| 241 | +process_frame = tk.Frame(root) |
| 242 | +process_frame.pack(pady=10, padx=10, fill=tk.BOTH, expand=True) |
| 243 | + |
| 244 | +process_label = tk.Label(process_frame, text="Process Log:") |
| 245 | +process_label.pack(side=tk.TOP, anchor='w') |
| 246 | + |
| 247 | +process_text = scrolledtext.ScrolledText(process_frame, wrap=tk.WORD, height=15) |
| 248 | +process_text.pack(expand=True, fill=tk.BOTH) |
| 249 | + |
| 250 | +root.mainloop() |
0 commit comments