gguf_editor_gui.py 63 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621
  1. #!/usr/bin/env python3
  2. from __future__ import annotations
  3. import logging
  4. import argparse
  5. import os
  6. import sys
  7. import numpy
  8. import enum
  9. from pathlib import Path
  10. from typing import Any, Optional, Tuple, Type
  11. import warnings
  12. import numpy as np
  13. from PySide6.QtWidgets import (
  14. QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
  15. QPushButton, QLabel, QLineEdit, QFileDialog, QTableWidget,
  16. QTableWidgetItem, QComboBox, QMessageBox, QTabWidget,
  17. QTextEdit, QFormLayout,
  18. QHeaderView, QDialog, QDialogButtonBox
  19. )
  20. from PySide6.QtCore import Qt
  21. # Necessary to load the local gguf package
  22. if "NO_LOCAL_GGUF" not in os.environ and (Path(__file__).parent.parent.parent.parent / 'gguf-py').exists():
  23. sys.path.insert(0, str(Path(__file__).parent.parent.parent))
  24. import gguf
  25. from gguf import GGUFReader, GGUFWriter, GGUFValueType, ReaderField
  26. from gguf.constants import TokenType, RopeScalingType, PoolingType, GGMLQuantizationType
  27. logger = logging.getLogger("gguf-editor-gui")
  28. # Map of key names to enum types for automatic enum interpretation
  29. KEY_TO_ENUM_TYPE = {
  30. gguf.Keys.Tokenizer.TOKEN_TYPE: TokenType,
  31. gguf.Keys.Rope.SCALING_TYPE: RopeScalingType,
  32. gguf.Keys.LLM.POOLING_TYPE: PoolingType,
  33. gguf.Keys.General.FILE_TYPE: GGMLQuantizationType,
  34. }
  35. # Define the tokenizer keys that should be edited together
  36. TOKENIZER_LINKED_KEYS = [
  37. gguf.Keys.Tokenizer.LIST,
  38. gguf.Keys.Tokenizer.TOKEN_TYPE,
  39. gguf.Keys.Tokenizer.SCORES
  40. ]
  41. class TokenizerEditorDialog(QDialog):
  42. def __init__(self, tokens, token_types, scores, parent=None):
  43. super().__init__(parent)
  44. self.setWindowTitle("Edit Tokenizer Data")
  45. self.resize(900, 600)
  46. self.tokens = tokens.copy() if tokens else []
  47. self.token_types = token_types.copy() if token_types else []
  48. self.scores = scores.copy() if scores else []
  49. # Ensure all arrays have the same length
  50. max_len = max(len(self.tokens), len(self.token_types), len(self.scores))
  51. if len(self.tokens) < max_len:
  52. self.tokens.extend([""] * (max_len - len(self.tokens)))
  53. if len(self.token_types) < max_len:
  54. self.token_types.extend([0] * (max_len - len(self.token_types)))
  55. if len(self.scores) < max_len:
  56. self.scores.extend([0.0] * (max_len - len(self.scores)))
  57. layout = QVBoxLayout(self)
  58. # Add filter controls
  59. filter_layout = QHBoxLayout()
  60. filter_layout.addWidget(QLabel("Filter:"))
  61. self.filter_edit = QLineEdit()
  62. self.filter_edit.setPlaceholderText("Type to filter tokens...")
  63. self.filter_edit.textChanged.connect(self.apply_filter)
  64. filter_layout.addWidget(self.filter_edit)
  65. # Add page controls
  66. self.page_size = 100 # Show 100 items per page
  67. self.current_page = 0
  68. self.total_pages = max(1, (len(self.tokens) + self.page_size - 1) // self.page_size)
  69. self.page_label = QLabel(f"Page 1 of {self.total_pages}")
  70. filter_layout.addWidget(self.page_label)
  71. prev_page = QPushButton("Previous")
  72. prev_page.clicked.connect(self.previous_page)
  73. filter_layout.addWidget(prev_page)
  74. next_page = QPushButton("Next")
  75. next_page.clicked.connect(self.next_page)
  76. filter_layout.addWidget(next_page)
  77. layout.addLayout(filter_layout)
  78. # Tokenizer data table
  79. self.tokens_table = QTableWidget()
  80. self.tokens_table.setColumnCount(4)
  81. self.tokens_table.setHorizontalHeaderLabels(["Index", "Token", "Type", "Score"])
  82. self.tokens_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
  83. self.tokens_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
  84. self.tokens_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
  85. self.tokens_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
  86. layout.addWidget(self.tokens_table)
  87. # Controls
  88. controls_layout = QHBoxLayout()
  89. add_button = QPushButton("Add Token")
  90. add_button.clicked.connect(self.add_token)
  91. controls_layout.addWidget(add_button)
  92. remove_button = QPushButton("Remove Selected")
  93. remove_button.clicked.connect(self.remove_selected)
  94. controls_layout.addWidget(remove_button)
  95. controls_layout.addStretch()
  96. layout.addLayout(controls_layout)
  97. # Buttons
  98. buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
  99. buttons.accepted.connect(self.accept)
  100. buttons.rejected.connect(self.reject)
  101. layout.addWidget(buttons)
  102. # Initialize the filtered values
  103. self.filtered_indices = list(range(len(self.tokens)))
  104. # Load data for the first page
  105. self.load_page()
  106. def apply_filter(self):
  107. """Filter the tokens based on the search text."""
  108. filter_text = self.filter_edit.text().lower()
  109. if not filter_text:
  110. # No filter, show all values
  111. self.filtered_indices = list(range(len(self.tokens)))
  112. else:
  113. # Apply filter
  114. self.filtered_indices = []
  115. for i, token in enumerate(self.tokens):
  116. if filter_text in str(token).lower():
  117. self.filtered_indices.append(i)
  118. # Reset to first page and reload
  119. self.total_pages = max(1, (len(self.filtered_indices) + self.page_size - 1) // self.page_size)
  120. self.current_page = 0
  121. self.page_label.setText(f"Page 1 of {self.total_pages}")
  122. self.load_page()
  123. def previous_page(self):
  124. """Go to the previous page of results."""
  125. if self.current_page > 0:
  126. self.current_page -= 1
  127. self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
  128. self.load_page()
  129. def next_page(self):
  130. """Go to the next page of results."""
  131. if self.current_page < self.total_pages - 1:
  132. self.current_page += 1
  133. self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
  134. self.load_page()
  135. def load_page(self):
  136. """Load the current page of tokenizer data."""
  137. self.tokens_table.setRowCount(0) # Clear the table
  138. # Calculate start and end indices for the current page
  139. start_idx = self.current_page * self.page_size
  140. end_idx = min(start_idx + self.page_size, len(self.filtered_indices))
  141. # Pre-allocate rows for better performance
  142. self.tokens_table.setRowCount(end_idx - start_idx)
  143. for row, i in enumerate(range(start_idx, end_idx)):
  144. orig_idx = self.filtered_indices[i]
  145. # Index
  146. index_item = QTableWidgetItem(str(orig_idx))
  147. index_item.setData(Qt.ItemDataRole.UserRole, orig_idx) # Store original index
  148. index_item.setFlags(index_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  149. self.tokens_table.setItem(row, 0, index_item)
  150. # Token
  151. token_item = QTableWidgetItem(str(self.tokens[orig_idx]))
  152. self.tokens_table.setItem(row, 1, token_item)
  153. # Token Type
  154. token_type = self.token_types[orig_idx] if orig_idx < len(self.token_types) else 0
  155. try:
  156. enum_val = TokenType(token_type)
  157. display_text = f"{enum_val.name} ({token_type})"
  158. except (ValueError, KeyError):
  159. display_text = f"Unknown ({token_type})"
  160. type_item = QTableWidgetItem(display_text)
  161. type_item.setData(Qt.ItemDataRole.UserRole, token_type)
  162. # Make type cell editable with a double-click handler
  163. type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  164. self.tokens_table.setItem(row, 2, type_item)
  165. # Score
  166. score = self.scores[orig_idx] if orig_idx < len(self.scores) else 0.0
  167. score_item = QTableWidgetItem(str(score))
  168. self.tokens_table.setItem(row, 3, score_item)
  169. # Connect double-click handler for token type cells
  170. self.tokens_table.cellDoubleClicked.connect(self.handle_cell_double_click)
  171. def handle_cell_double_click(self, row, column):
  172. """Handle double-click on a cell, specifically for token type editing."""
  173. if column == 2: # Token Type column
  174. orig_item = self.tokens_table.item(row, 0)
  175. if orig_item:
  176. orig_idx = orig_item.data(Qt.ItemDataRole.UserRole)
  177. self.edit_token_type(row, orig_idx)
  178. def edit_token_type(self, row, orig_idx):
  179. """Edit a token type using a dialog with a dropdown of all enum options."""
  180. current_value = self.token_types[orig_idx] if orig_idx < len(self.token_types) else 0
  181. # Create a dialog with enum options
  182. dialog = QDialog(self)
  183. dialog.setWindowTitle("Select Token Type")
  184. layout = QVBoxLayout(dialog)
  185. combo = QComboBox()
  186. for enum_val in TokenType:
  187. combo.addItem(f"{enum_val.name} ({enum_val.value})", enum_val.value)
  188. # Set current value
  189. try:
  190. if isinstance(current_value, int):
  191. enum_val = TokenType(current_value)
  192. combo.setCurrentText(f"{enum_val.name} ({current_value})")
  193. except (ValueError, KeyError):
  194. pass
  195. layout.addWidget(combo)
  196. buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
  197. buttons.accepted.connect(dialog.accept)
  198. buttons.rejected.connect(dialog.reject)
  199. layout.addWidget(buttons)
  200. if dialog.exec() == QDialog.DialogCode.Accepted:
  201. # Get the selected value
  202. new_value = combo.currentData()
  203. enum_val = TokenType(new_value)
  204. display_text = f"{enum_val.name} ({new_value})"
  205. # Update the display
  206. type_item = self.tokens_table.item(row, 2)
  207. if type_item:
  208. type_item.setText(display_text)
  209. type_item.setData(Qt.ItemDataRole.UserRole, new_value)
  210. # Update the actual value
  211. self.token_types[orig_idx] = new_value
  212. def add_token(self):
  213. """Add a new token to the end of the list."""
  214. # Add to the end of the arrays
  215. self.tokens.append("")
  216. self.token_types.append(0) # Default to normal token
  217. self.scores.append(0.0)
  218. orig_idx = len(self.tokens) - 1
  219. # Add to filtered indices if it matches the current filter
  220. filter_text = self.filter_edit.text().lower()
  221. if not filter_text or filter_text in "":
  222. self.filtered_indices.append(orig_idx)
  223. # Update pagination
  224. self.total_pages = max(1, (len(self.filtered_indices) + self.page_size - 1) // self.page_size)
  225. # Go to the last page to show the new item
  226. self.current_page = self.total_pages - 1
  227. self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
  228. # Reload the page
  229. self.load_page()
  230. def remove_selected(self):
  231. """Remove selected tokens from all arrays."""
  232. selected_rows = []
  233. for item in self.tokens_table.selectedItems():
  234. row = item.row()
  235. if row not in selected_rows:
  236. selected_rows.append(row)
  237. if not selected_rows:
  238. return
  239. # Get original indices in descending order to avoid index shifting
  240. orig_indices = []
  241. for row in selected_rows:
  242. orig_item = self.tokens_table.item(row, 0)
  243. if orig_item:
  244. orig_indices.append(orig_item.data(Qt.ItemDataRole.UserRole))
  245. orig_indices.sort(reverse=True)
  246. # Remove from all arrays
  247. for idx in orig_indices:
  248. if idx < len(self.tokens):
  249. del self.tokens[idx]
  250. if idx < len(self.token_types):
  251. del self.token_types[idx]
  252. if idx < len(self.scores):
  253. del self.scores[idx]
  254. # Rebuild filtered_indices
  255. self.filtered_indices = []
  256. filter_text = self.filter_edit.text().lower()
  257. for i, token in enumerate(self.tokens):
  258. if not filter_text or filter_text in str(token).lower():
  259. self.filtered_indices.append(i)
  260. # Update pagination
  261. self.total_pages = max(1, (len(self.filtered_indices) + self.page_size - 1) // self.page_size)
  262. self.current_page = min(self.current_page, self.total_pages - 1)
  263. self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
  264. # Reload the page
  265. self.load_page()
  266. def get_data(self):
  267. """Return the edited tokenizer data."""
  268. return self.tokens, self.token_types, self.scores
  269. class ArrayEditorDialog(QDialog):
  270. def __init__(self, array_values, element_type, key=None, parent=None):
  271. super().__init__(parent)
  272. self.setWindowTitle("Edit Array Values")
  273. self.resize(700, 500)
  274. self.array_values = array_values
  275. self.element_type = element_type
  276. self.key = key
  277. # Get enum type for this array if applicable
  278. self.enum_type = None
  279. if key in KEY_TO_ENUM_TYPE and element_type == GGUFValueType.INT32:
  280. self.enum_type = KEY_TO_ENUM_TYPE[key]
  281. layout = QVBoxLayout(self)
  282. # Add enum type information if applicable
  283. if self.enum_type is not None:
  284. enum_info_layout = QHBoxLayout()
  285. enum_label = QLabel(f"Editing {self.enum_type.__name__} values:")
  286. enum_info_layout.addWidget(enum_label)
  287. # Add a legend for the enum values
  288. enum_values = ", ".join([f"{e.name}={e.value}" for e in self.enum_type])
  289. enum_values_label = QLabel(f"Available values: {enum_values}")
  290. enum_values_label.setWordWrap(True)
  291. enum_info_layout.addWidget(enum_values_label, 1)
  292. layout.addLayout(enum_info_layout)
  293. # Add search/filter controls
  294. filter_layout = QHBoxLayout()
  295. filter_layout.addWidget(QLabel("Filter:"))
  296. self.filter_edit = QLineEdit()
  297. self.filter_edit.setPlaceholderText("Type to filter values...")
  298. self.filter_edit.textChanged.connect(self.apply_filter)
  299. filter_layout.addWidget(self.filter_edit)
  300. # Add page controls for large arrays
  301. self.page_size = 100 # Show 100 items per page
  302. self.current_page = 0
  303. self.total_pages = max(1, (len(array_values) + self.page_size - 1) // self.page_size)
  304. self.page_label = QLabel(f"Page 1 of {self.total_pages}")
  305. filter_layout.addWidget(self.page_label)
  306. prev_page = QPushButton("Previous")
  307. prev_page.clicked.connect(self.previous_page)
  308. filter_layout.addWidget(prev_page)
  309. next_page = QPushButton("Next")
  310. next_page.clicked.connect(self.next_page)
  311. filter_layout.addWidget(next_page)
  312. layout.addLayout(filter_layout)
  313. # Array items table
  314. self.items_table = QTableWidget()
  315. # Set up columns based on whether we have an enum type
  316. if self.enum_type is not None:
  317. self.items_table.setColumnCount(3)
  318. self.items_table.setHorizontalHeaderLabels(["Index", "Value", "Actions"])
  319. self.items_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
  320. self.items_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
  321. self.items_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
  322. else:
  323. self.items_table.setColumnCount(2)
  324. self.items_table.setHorizontalHeaderLabels(["Index", "Value"])
  325. self.items_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
  326. self.items_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
  327. layout.addWidget(self.items_table)
  328. # Controls
  329. controls_layout = QHBoxLayout()
  330. add_button = QPushButton("Add Item")
  331. add_button.clicked.connect(self.add_item)
  332. controls_layout.addWidget(add_button)
  333. remove_button = QPushButton("Remove Selected")
  334. remove_button.clicked.connect(self.remove_selected)
  335. controls_layout.addWidget(remove_button)
  336. # Add bulk edit button for enum arrays
  337. if self.enum_type is not None:
  338. bulk_edit_button = QPushButton("Bulk Edit Selected")
  339. bulk_edit_button.clicked.connect(self.bulk_edit_selected)
  340. controls_layout.addWidget(bulk_edit_button)
  341. controls_layout.addStretch()
  342. layout.addLayout(controls_layout)
  343. # Buttons
  344. buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
  345. buttons.accepted.connect(self.accept)
  346. buttons.rejected.connect(self.reject)
  347. layout.addWidget(buttons)
  348. # Initialize the filtered values
  349. self.filtered_indices = list(range(len(self.array_values)))
  350. # Load array values for the first page
  351. self.load_page()
  352. def apply_filter(self):
  353. """Filter the array values based on the search text."""
  354. filter_text = self.filter_edit.text().lower()
  355. if not filter_text:
  356. # No filter, show all values
  357. self.filtered_indices = list(range(len(self.array_values)))
  358. else:
  359. # Apply filter
  360. self.filtered_indices = []
  361. for i, value in enumerate(self.array_values):
  362. # For enum values, search in both name and value
  363. if self.enum_type is not None and isinstance(value, int):
  364. try:
  365. enum_val = self.enum_type(value)
  366. display_text = f"{enum_val.name} ({value})".lower()
  367. if filter_text in display_text:
  368. self.filtered_indices.append(i)
  369. except (ValueError, KeyError):
  370. # If not a valid enum value, just check the raw value
  371. if filter_text in str(value).lower():
  372. self.filtered_indices.append(i)
  373. else:
  374. # For non-enum values, just check the string representation
  375. if filter_text in str(value).lower():
  376. self.filtered_indices.append(i)
  377. # Reset to first page and reload
  378. self.total_pages = max(1, (len(self.filtered_indices) + self.page_size - 1) // self.page_size)
  379. self.current_page = 0
  380. self.page_label.setText(f"Page 1 of {self.total_pages}")
  381. self.load_page()
  382. def previous_page(self):
  383. """Go to the previous page of results."""
  384. if self.current_page > 0:
  385. self.current_page -= 1
  386. self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
  387. self.load_page()
  388. def next_page(self):
  389. """Go to the next page of results."""
  390. if self.current_page < self.total_pages - 1:
  391. self.current_page += 1
  392. self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
  393. self.load_page()
  394. def load_page(self):
  395. """Load the current page of array values."""
  396. self.items_table.setRowCount(0) # Clear the table
  397. # Calculate start and end indices for the current page
  398. start_idx = self.current_page * self.page_size
  399. end_idx = min(start_idx + self.page_size, len(self.filtered_indices))
  400. # Pre-allocate rows for better performance
  401. self.items_table.setRowCount(end_idx - start_idx)
  402. for row, i in enumerate(range(start_idx, end_idx)):
  403. orig_idx = self.filtered_indices[i]
  404. value = self.array_values[orig_idx]
  405. # Index
  406. index_item = QTableWidgetItem(str(orig_idx))
  407. index_item.setData(Qt.ItemDataRole.UserRole, orig_idx) # Store original index
  408. index_item.setFlags(index_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  409. self.items_table.setItem(row, 0, index_item)
  410. # Value
  411. if self.enum_type is not None:
  412. # Display enum value and name
  413. try:
  414. if isinstance(value, (int, numpy.signedinteger)):
  415. enum_val = self.enum_type(value)
  416. display_text = f"{enum_val.name} ({value})"
  417. else:
  418. display_text = str(value)
  419. except (ValueError, KeyError):
  420. display_text = f"Unknown ({value})"
  421. # Store the enum value in the item
  422. value_item = QTableWidgetItem(display_text)
  423. value_item.setData(Qt.ItemDataRole.UserRole, value)
  424. value_item.setFlags(value_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  425. self.items_table.setItem(row, 1, value_item)
  426. # Add an edit button in a separate column
  427. edit_button = QPushButton("Edit")
  428. edit_button.setProperty("row", row)
  429. edit_button.clicked.connect(self.edit_array_enum_value)
  430. # Create a widget to hold the button
  431. button_widget = QWidget()
  432. button_layout = QHBoxLayout(button_widget)
  433. button_layout.setContentsMargins(2, 2, 2, 2)
  434. button_layout.addWidget(edit_button)
  435. button_layout.addStretch()
  436. self.items_table.setCellWidget(row, 2, button_widget)
  437. else:
  438. value_item = QTableWidgetItem(str(value))
  439. self.items_table.setItem(row, 1, value_item)
  440. def edit_array_enum_value(self):
  441. """Handle editing an enum value in the array editor."""
  442. button = self.sender()
  443. row = button.property("row")
  444. # Get the original index from the table item
  445. orig_item = self.items_table.item(row, 0)
  446. new_item = self.items_table.item(row, 1)
  447. if orig_item and new_item and self.enum_type and self.edit_enum_value(row, self.enum_type):
  448. orig_idx = orig_item.data(Qt.ItemDataRole.UserRole)
  449. new_value = new_item.data(Qt.ItemDataRole.UserRole)
  450. # Update the stored value in the array
  451. if isinstance(new_value, (int, float, str, bool)):
  452. self.array_values[orig_idx] = new_value
  453. def bulk_edit_selected(self):
  454. """Edit multiple enum values at once."""
  455. if not self.enum_type:
  456. return
  457. selected_rows = set()
  458. for item in self.items_table.selectedItems():
  459. selected_rows.add(item.row())
  460. if not selected_rows:
  461. QMessageBox.information(self, "No Selection", "Please select at least one row to edit.")
  462. return
  463. # Create a dialog with enum options
  464. dialog = QDialog(self)
  465. dialog.setWindowTitle(f"Bulk Edit {self.enum_type.__name__} Values")
  466. layout = QVBoxLayout(dialog)
  467. layout.addWidget(QLabel(f"Set {len(selected_rows)} selected items to:"))
  468. combo = QComboBox()
  469. for enum_val in self.enum_type:
  470. combo.addItem(f"{enum_val.name} ({enum_val.value})", enum_val.value)
  471. layout.addWidget(combo)
  472. buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
  473. buttons.accepted.connect(dialog.accept)
  474. buttons.rejected.connect(dialog.reject)
  475. layout.addWidget(buttons)
  476. if dialog.exec() == QDialog.DialogCode.Accepted:
  477. # Get the selected value
  478. new_value = combo.currentData()
  479. enum_val = self.enum_type(new_value)
  480. display_text = f"{enum_val.name} ({new_value})"
  481. # Update all selected rows
  482. for row in selected_rows:
  483. orig_item = self.items_table.item(row, 0)
  484. new_item = self.items_table.item(row, 1)
  485. if orig_item and new_item:
  486. orig_idx = orig_item.data(Qt.ItemDataRole.UserRole)
  487. self.array_values[orig_idx] = new_value
  488. # Update the display
  489. new_item.setText(display_text)
  490. new_item.setData(Qt.ItemDataRole.UserRole, new_value)
  491. def add_item(self):
  492. # Add to the end of the array
  493. orig_idx = len(self.array_values)
  494. # Add default value based on type
  495. if self.enum_type is not None:
  496. # Default to first enum value
  497. default_value = list(self.enum_type)[0].value
  498. self.array_values.append(default_value)
  499. else:
  500. if self.element_type == GGUFValueType.STRING:
  501. self.array_values.append("")
  502. else:
  503. self.array_values.append(0)
  504. # Add to filtered indices if it matches the current filter
  505. self.filtered_indices.append(orig_idx)
  506. # Update pagination
  507. self.total_pages = max(1, (len(self.filtered_indices) + self.page_size - 1) // self.page_size)
  508. # Go to the last page to show the new item
  509. self.current_page = self.total_pages - 1
  510. self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
  511. # Reload the page
  512. self.load_page()
  513. def remove_selected(self):
  514. selected_rows = []
  515. for item in self.items_table.selectedItems():
  516. row = item.row()
  517. if row not in selected_rows:
  518. selected_rows.append(row)
  519. if not selected_rows:
  520. return
  521. # Get original indices in descending order to avoid index shifting
  522. orig_indices = list()
  523. for row in selected_rows:
  524. orig_item = self.items_table.item(row, 0)
  525. if orig_item:
  526. orig_indices.append(orig_item.data(Qt.ItemDataRole.UserRole))
  527. orig_indices.sort(reverse=True)
  528. # Remove from array_values
  529. for idx in orig_indices:
  530. del self.array_values[idx]
  531. # Rebuild filtered_indices
  532. self.filtered_indices = []
  533. filter_text = self.filter_edit.text().lower()
  534. for i, value in enumerate(self.array_values):
  535. if not filter_text:
  536. self.filtered_indices.append(i)
  537. else:
  538. # Apply filter
  539. if self.enum_type is not None and isinstance(value, int):
  540. try:
  541. enum_val = self.enum_type(value)
  542. display_text = f"{enum_val.name} ({value})".lower()
  543. if filter_text in display_text:
  544. self.filtered_indices.append(i)
  545. except (ValueError, KeyError):
  546. if filter_text in str(value).lower():
  547. self.filtered_indices.append(i)
  548. else:
  549. if filter_text in str(value).lower():
  550. self.filtered_indices.append(i)
  551. # Update pagination
  552. self.total_pages = max(1, (len(self.filtered_indices) + self.page_size - 1) // self.page_size)
  553. self.current_page = min(self.current_page, self.total_pages - 1)
  554. self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
  555. # Reload the page
  556. self.load_page()
  557. def edit_enum_value(self, row: int, enum_type: Type[enum.Enum]):
  558. """Edit an enum value using a dialog with a dropdown of all enum options."""
  559. # Get the original index from the table item
  560. orig_item = self.items_table.item(row, 0)
  561. if orig_item:
  562. orig_idx = orig_item.data(Qt.ItemDataRole.UserRole)
  563. else:
  564. return
  565. current_value = self.array_values[orig_idx]
  566. # Create a dialog with enum options
  567. dialog = QDialog(self)
  568. dialog.setWindowTitle(f"Select {enum_type.__name__} Value")
  569. layout = QVBoxLayout(dialog)
  570. # Add description
  571. description = QLabel(f"Select a {enum_type.__name__} value:")
  572. layout.addWidget(description)
  573. # Use a combo box for quick selection
  574. combo = QComboBox()
  575. for enum_val in enum_type:
  576. combo.addItem(f"{enum_val.name} ({enum_val.value})", enum_val.value)
  577. # Set current value
  578. try:
  579. if isinstance(current_value, int):
  580. enum_val = enum_type(current_value)
  581. combo.setCurrentText(f"{enum_val.name} ({current_value})")
  582. except (ValueError, KeyError):
  583. pass
  584. layout.addWidget(combo)
  585. buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
  586. buttons.accepted.connect(dialog.accept)
  587. buttons.rejected.connect(dialog.reject)
  588. layout.addWidget(buttons)
  589. if dialog.exec() == QDialog.DialogCode.Accepted:
  590. # Update the value display and stored data
  591. new_value = combo.currentData()
  592. enum_val = enum_type(new_value)
  593. display_text = f"{enum_val.name} ({new_value})"
  594. new_item = self.items_table.item(row, 1)
  595. if new_item:
  596. new_item.setText(display_text)
  597. new_item.setData(Qt.ItemDataRole.UserRole, new_value)
  598. # Update the actual array value
  599. self.array_values[orig_idx] = new_value
  600. return True
  601. return False
  602. def get_array_values(self):
  603. # The array_values list is kept up-to-date as edits are made
  604. return self.array_values
  605. class AddMetadataDialog(QDialog):
  606. def __init__(self, parent=None):
  607. super().__init__(parent)
  608. self.setWindowTitle("Add Metadata")
  609. self.resize(400, 200)
  610. layout = QVBoxLayout(self)
  611. form_layout = QFormLayout()
  612. self.key_edit = QLineEdit()
  613. form_layout.addRow("Key:", self.key_edit)
  614. self.type_combo = QComboBox()
  615. for value_type in GGUFValueType:
  616. if value_type != GGUFValueType.ARRAY: # Skip array type for simplicity
  617. self.type_combo.addItem(value_type.name, value_type)
  618. form_layout.addRow("Type:", self.type_combo)
  619. self.value_edit = QTextEdit()
  620. form_layout.addRow("Value:", self.value_edit)
  621. layout.addLayout(form_layout)
  622. buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
  623. buttons.accepted.connect(self.accept)
  624. buttons.rejected.connect(self.reject)
  625. layout.addWidget(buttons)
  626. def get_data(self) -> Tuple[str, GGUFValueType, Any]:
  627. key = self.key_edit.text()
  628. value_type = self.type_combo.currentData()
  629. value_text = self.value_edit.toPlainText()
  630. # Convert value based on type
  631. if value_type == GGUFValueType.UINT8:
  632. value = np.uint8(int(value_text))
  633. elif value_type == GGUFValueType.INT8:
  634. value = np.int8(int(value_text))
  635. elif value_type == GGUFValueType.UINT16:
  636. value = np.uint16(int(value_text))
  637. elif value_type == GGUFValueType.INT16:
  638. value = np.int16(int(value_text))
  639. elif value_type == GGUFValueType.UINT32:
  640. value = np.uint32(int(value_text))
  641. elif value_type == GGUFValueType.INT32:
  642. value = np.int32(int(value_text))
  643. elif value_type == GGUFValueType.FLOAT32:
  644. value = np.float32(float(value_text))
  645. elif value_type == GGUFValueType.BOOL:
  646. value = value_text.lower() in ('true', 'yes', '1')
  647. elif value_type == GGUFValueType.STRING:
  648. value = value_text
  649. else:
  650. value = value_text
  651. return key, value_type, value
  652. class GGUFEditorWindow(QMainWindow):
  653. def __init__(self):
  654. super().__init__()
  655. self.setWindowTitle("GGUF Editor")
  656. self.resize(1000, 800)
  657. self.current_file = None
  658. self.reader = None
  659. self.modified = False
  660. self.metadata_changes = {} # Store changes to apply when saving
  661. self.metadata_to_remove = set() # Store keys to remove when saving
  662. self.on_metadata_changed_is_connected = False
  663. self.setup_ui()
  664. def setup_ui(self):
  665. central_widget = QWidget()
  666. self.setCentralWidget(central_widget)
  667. main_layout = QVBoxLayout(central_widget)
  668. # File controls
  669. file_layout = QHBoxLayout()
  670. self.file_path_edit = QLineEdit()
  671. self.file_path_edit.setReadOnly(True)
  672. file_layout.addWidget(self.file_path_edit)
  673. open_button = QPushButton("Open GGUF")
  674. open_button.clicked.connect(self.open_file)
  675. file_layout.addWidget(open_button)
  676. save_button = QPushButton("Save As...")
  677. save_button.clicked.connect(self.save_file)
  678. file_layout.addWidget(save_button)
  679. main_layout.addLayout(file_layout)
  680. # Tabs for different views
  681. self.tabs = QTabWidget()
  682. # Metadata tab
  683. self.metadata_tab = QWidget()
  684. metadata_layout = QVBoxLayout(self.metadata_tab)
  685. # Metadata table
  686. self.metadata_table = QTableWidget()
  687. self.metadata_table.setColumnCount(4)
  688. self.metadata_table.setHorizontalHeaderLabels(["Key", "Type", "Value", "Actions"])
  689. self.metadata_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
  690. self.metadata_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
  691. self.metadata_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
  692. self.metadata_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
  693. metadata_layout.addWidget(self.metadata_table)
  694. # Metadata controls
  695. metadata_controls = QHBoxLayout()
  696. add_metadata_button = QPushButton("Add Metadata")
  697. add_metadata_button.clicked.connect(self.add_metadata)
  698. metadata_controls.addWidget(add_metadata_button)
  699. metadata_controls.addStretch()
  700. metadata_layout.addLayout(metadata_controls)
  701. # Tensors tab
  702. self.tensors_tab = QWidget()
  703. tensors_layout = QVBoxLayout(self.tensors_tab)
  704. self.tensors_table = QTableWidget()
  705. self.tensors_table.setColumnCount(5)
  706. self.tensors_table.setHorizontalHeaderLabels(["Name", "Type", "Shape", "Elements", "Size (bytes)"])
  707. self.tensors_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
  708. self.tensors_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
  709. self.tensors_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
  710. self.tensors_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
  711. self.tensors_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents)
  712. tensors_layout.addWidget(self.tensors_table)
  713. # Add tabs to tab widget
  714. self.tabs.addTab(self.metadata_tab, "Metadata")
  715. self.tabs.addTab(self.tensors_tab, "Tensors")
  716. main_layout.addWidget(self.tabs)
  717. # Status bar
  718. self.statusBar().showMessage("Ready")
  719. def load_file(self, file_path):
  720. """Load a GGUF file by path"""
  721. try:
  722. self.statusBar().showMessage(f"Loading {file_path}...")
  723. QApplication.processEvents()
  724. self.reader = GGUFReader(file_path, 'r')
  725. self.current_file = file_path
  726. self.file_path_edit.setText(file_path)
  727. self.load_metadata()
  728. self.load_tensors()
  729. self.metadata_changes = {}
  730. self.metadata_to_remove = set()
  731. self.modified = False
  732. self.statusBar().showMessage(f"Loaded {file_path}")
  733. return True
  734. except Exception as e:
  735. QMessageBox.critical(self, "Error", f"Failed to open file: {str(e)}")
  736. self.statusBar().showMessage("Error loading file")
  737. return False
  738. def open_file(self):
  739. file_path, _ = QFileDialog.getOpenFileName(
  740. self, "Open GGUF File", "", "GGUF Files (*.gguf);;All Files (*)"
  741. )
  742. if not file_path:
  743. return
  744. self.load_file(file_path)
  745. def load_metadata(self):
  746. self.metadata_table.setRowCount(0)
  747. if not self.reader:
  748. return
  749. # Disconnect to prevent triggering during loading
  750. if self.on_metadata_changed_is_connected:
  751. with warnings.catch_warnings():
  752. warnings.filterwarnings('ignore')
  753. self.metadata_table.itemChanged.disconnect(self.on_metadata_changed)
  754. self.on_metadata_changed_is_connected = False
  755. for i, (key, field) in enumerate(self.reader.fields.items()):
  756. self.metadata_table.insertRow(i)
  757. # Key
  758. key_item = QTableWidgetItem(key)
  759. key_item.setFlags(key_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  760. self.metadata_table.setItem(i, 0, key_item)
  761. # Type
  762. if not field.types:
  763. type_str = "N/A"
  764. elif field.types[0] == GGUFValueType.ARRAY:
  765. nest_count = len(field.types) - 1
  766. element_type = field.types[-1].name
  767. # Check if this is an enum array
  768. enum_type = self.get_enum_for_key(key)
  769. if enum_type is not None and field.types[-1] == GGUFValueType.INT32:
  770. element_type = enum_type.__name__
  771. type_str = '[' * nest_count + element_type + ']' * nest_count
  772. else:
  773. type_str = str(field.types[0].name)
  774. # Check if this is an enum field
  775. enum_type = self.get_enum_for_key(key)
  776. if enum_type is not None and field.types[0] == GGUFValueType.INT32:
  777. type_str = enum_type.__name__
  778. type_item = QTableWidgetItem(type_str)
  779. type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  780. self.metadata_table.setItem(i, 1, type_item)
  781. # Value
  782. value_str = self.format_field_value(field)
  783. value_item = QTableWidgetItem(value_str)
  784. # Make only simple values editable
  785. if len(field.types) == 1 and field.types[0] != GGUFValueType.ARRAY:
  786. value_item.setFlags(value_item.flags() | Qt.ItemFlag.ItemIsEditable)
  787. else:
  788. value_item.setFlags(value_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  789. self.metadata_table.setItem(i, 2, value_item)
  790. # Actions
  791. actions_widget = QWidget()
  792. actions_layout = QHBoxLayout(actions_widget)
  793. actions_layout.setContentsMargins(2, 2, 2, 2)
  794. # Add Edit button for arrays and enum fields
  795. if field.types and field.types[0] == GGUFValueType.ARRAY:
  796. edit_button = QPushButton("Edit")
  797. edit_button.setProperty("row", i)
  798. edit_button.setProperty("key", key)
  799. edit_button.clicked.connect(self.edit_array_metadata)
  800. actions_layout.addWidget(edit_button)
  801. # Add special label for tokenizer linked fields
  802. if key in TOKENIZER_LINKED_KEYS:
  803. edit_button.setText("Edit Tokenizer")
  804. edit_button.setToolTip("Edit all tokenizer data together")
  805. elif len(field.types) == 1 and self.get_enum_for_key(key) is not None:
  806. edit_button = QPushButton("Edit")
  807. edit_button.setProperty("row", i)
  808. edit_button.setProperty("key", key)
  809. edit_button.clicked.connect(self.edit_metadata_enum)
  810. actions_layout.addWidget(edit_button)
  811. remove_button = QPushButton("Remove")
  812. remove_button.setProperty("row", i)
  813. remove_button.setProperty("key", key)
  814. remove_button.clicked.connect(self.remove_metadata)
  815. actions_layout.addWidget(remove_button)
  816. self.metadata_table.setCellWidget(i, 3, actions_widget)
  817. # Reconnect after loading
  818. self.metadata_table.itemChanged.connect(self.on_metadata_changed)
  819. self.on_metadata_changed_is_connected = True
  820. def extract_array_values(self, field: ReaderField) -> list:
  821. """Extract all values from an array field."""
  822. if not field.types or field.types[0] != GGUFValueType.ARRAY:
  823. return []
  824. curr_type = field.types[1]
  825. array_values = []
  826. total_elements = len(field.data)
  827. if curr_type == GGUFValueType.STRING:
  828. for element_pos in range(total_elements):
  829. value_string = str(bytes(field.parts[-1 - (total_elements - element_pos - 1) * 2]), encoding='utf-8')
  830. array_values.append(value_string)
  831. elif self.reader and curr_type in self.reader.gguf_scalar_to_np:
  832. for element_pos in range(total_elements):
  833. array_values.append(field.parts[-1 - (total_elements - element_pos - 1)][0])
  834. return array_values
  835. def get_enum_for_key(self, key: str) -> Optional[Type[enum.Enum]]:
  836. """Get the enum type for a given key if it exists."""
  837. return KEY_TO_ENUM_TYPE.get(key)
  838. def format_enum_value(self, value: Any, enum_type: Type[enum.Enum]) -> str:
  839. """Format a value as an enum if possible."""
  840. try:
  841. if isinstance(value, (int, str)):
  842. enum_value = enum_type(value)
  843. return f"{enum_value.name} ({value})"
  844. except (ValueError, KeyError):
  845. pass
  846. return str(value)
  847. def format_field_value(self, field: ReaderField) -> str:
  848. if not field.types:
  849. return "N/A"
  850. if len(field.types) == 1:
  851. curr_type = field.types[0]
  852. if curr_type == GGUFValueType.STRING:
  853. return str(bytes(field.parts[-1]), encoding='utf-8')
  854. elif self.reader and curr_type in self.reader.gguf_scalar_to_np:
  855. value = field.parts[-1][0]
  856. # Check if this field has an enum type
  857. enum_type = self.get_enum_for_key(field.name)
  858. if enum_type is not None:
  859. return self.format_enum_value(value, enum_type)
  860. return str(value)
  861. if field.types[0] == GGUFValueType.ARRAY:
  862. array_values = self.extract_array_values(field)
  863. render_element = min(5, len(array_values))
  864. # Get enum type for this array if applicable
  865. enum_type = self.get_enum_for_key(field.name)
  866. if enum_type is not None:
  867. array_elements = []
  868. for i in range(render_element):
  869. array_elements.append(self.format_enum_value(array_values[i], enum_type))
  870. else:
  871. array_elements = [str(array_values[i]) for i in range(render_element)]
  872. return f"[ {', '.join(array_elements).strip()}{', ...' if len(array_values) > len(array_elements) else ''} ]"
  873. return "Complex value"
  874. def load_tensors(self):
  875. self.tensors_table.setRowCount(0)
  876. if not self.reader:
  877. return
  878. for i, tensor in enumerate(self.reader.tensors):
  879. self.tensors_table.insertRow(i)
  880. # Name
  881. name_item = QTableWidgetItem(tensor.name)
  882. name_item.setFlags(name_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  883. self.tensors_table.setItem(i, 0, name_item)
  884. # Type
  885. type_item = QTableWidgetItem(tensor.tensor_type.name)
  886. type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  887. self.tensors_table.setItem(i, 1, type_item)
  888. # Shape
  889. shape_str = " × ".join(str(d) for d in tensor.shape)
  890. shape_item = QTableWidgetItem(shape_str)
  891. shape_item.setFlags(shape_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  892. self.tensors_table.setItem(i, 2, shape_item)
  893. # Elements
  894. elements_item = QTableWidgetItem(str(tensor.n_elements))
  895. elements_item.setFlags(elements_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  896. self.tensors_table.setItem(i, 3, elements_item)
  897. # Size
  898. size_item = QTableWidgetItem(f"{tensor.n_bytes:,}")
  899. size_item.setFlags(size_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  900. self.tensors_table.setItem(i, 4, size_item)
  901. def on_metadata_changed(self, item):
  902. if item.column() != 2: # Only handle value column changes
  903. return
  904. row = item.row()
  905. orig_item = self.metadata_table.item(row, 0)
  906. key = None
  907. if orig_item:
  908. key = orig_item.text()
  909. new_value = item.text()
  910. field = None
  911. if self.reader and key:
  912. field = self.reader.get_field(key)
  913. if not field or not field.types or not key:
  914. return
  915. value_type = field.types[0]
  916. # Check if this is an enum field
  917. enum_type = self.get_enum_for_key(key)
  918. if enum_type is not None and value_type == GGUFValueType.INT32:
  919. # Try to parse the enum value from the text
  920. try:
  921. # Check if it's a name
  922. try:
  923. enum_val = enum_type[new_value]
  924. converted_value = enum_val.value
  925. except (KeyError, AttributeError):
  926. # Check if it's a number or "NAME (value)" format
  927. if '(' in new_value and ')' in new_value:
  928. # Extract the value from "NAME (value)" format
  929. value_part = new_value.split('(')[1].split(')')[0].strip()
  930. converted_value = int(value_part)
  931. else:
  932. # Try to convert directly to int
  933. converted_value = int(new_value)
  934. # Validate that it's a valid enum value
  935. enum_type(converted_value)
  936. # Store the change
  937. self.metadata_changes[key] = (value_type, converted_value)
  938. self.modified = True
  939. # Update display with formatted enum value
  940. formatted_value = self.format_enum_value(converted_value, enum_type)
  941. item.setText(formatted_value)
  942. self.statusBar().showMessage(f"Changed {key} to {formatted_value}")
  943. return
  944. except (ValueError, KeyError) as e:
  945. QMessageBox.warning(
  946. self,
  947. f"Invalid Enum Value ({e})",
  948. f"'{new_value}' is not a valid {enum_type.__name__} value.\n"
  949. f"Valid values are: {', '.join(v.name for v in enum_type)}")
  950. # Revert to original value
  951. original_value = self.format_field_value(field)
  952. item.setText(original_value)
  953. return
  954. try:
  955. # Convert the string value to the appropriate type
  956. if value_type == GGUFValueType.UINT8:
  957. converted_value = np.uint8(int(new_value))
  958. elif value_type == GGUFValueType.INT8:
  959. converted_value = np.int8(int(new_value))
  960. elif value_type == GGUFValueType.UINT16:
  961. converted_value = np.uint16(int(new_value))
  962. elif value_type == GGUFValueType.INT16:
  963. converted_value = np.int16(int(new_value))
  964. elif value_type == GGUFValueType.UINT32:
  965. converted_value = np.uint32(int(new_value))
  966. elif value_type == GGUFValueType.INT32:
  967. converted_value = np.int32(int(new_value))
  968. elif value_type == GGUFValueType.FLOAT32:
  969. converted_value = np.float32(float(new_value))
  970. elif value_type == GGUFValueType.BOOL:
  971. converted_value = new_value.lower() in ('true', 'yes', '1')
  972. elif value_type == GGUFValueType.STRING:
  973. converted_value = new_value
  974. else:
  975. # Unsupported type for editing
  976. return
  977. # Store the change
  978. self.metadata_changes[key] = (value_type, converted_value)
  979. self.modified = True
  980. self.statusBar().showMessage(f"Changed {key} to {new_value}")
  981. except ValueError:
  982. QMessageBox.warning(self, "Invalid Value", f"The value '{new_value}' is not valid for type {value_type.name}")
  983. # Revert to original value
  984. original_value = self.format_field_value(field)
  985. item.setText(original_value)
  986. def remove_metadata(self):
  987. button = self.sender()
  988. key = button.property("key")
  989. row = button.property("row")
  990. reply = QMessageBox.question(
  991. self, "Confirm Removal",
  992. f"Are you sure you want to remove the metadata key '{key}'?",
  993. QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No
  994. )
  995. if reply == QMessageBox.StandardButton.Yes:
  996. self.metadata_table.removeRow(row)
  997. self.metadata_to_remove.add(key)
  998. # If we previously had changes for this key, remove them
  999. if key in self.metadata_changes:
  1000. del self.metadata_changes[key]
  1001. self.modified = True
  1002. self.statusBar().showMessage(f"Marked {key} for removal")
  1003. def edit_metadata_enum(self):
  1004. """Edit an enum metadata field."""
  1005. button = self.sender()
  1006. key = button.property("key")
  1007. row = button.property("row")
  1008. field = None
  1009. if self.reader:
  1010. field = self.reader.get_field(key)
  1011. if not field or not field.types:
  1012. return
  1013. enum_type = self.get_enum_for_key(key)
  1014. if enum_type is None:
  1015. return
  1016. # Get current value
  1017. current_value = field.contents()
  1018. # Create a dialog with enum options
  1019. dialog = QDialog(self)
  1020. dialog.setWindowTitle(f"Select {enum_type.__name__} Value")
  1021. layout = QVBoxLayout(dialog)
  1022. combo = QComboBox()
  1023. for enum_val in enum_type:
  1024. combo.addItem(f"{enum_val.name} ({enum_val.value})", enum_val.value)
  1025. # Set current value
  1026. try:
  1027. if isinstance(current_value, (int, str)):
  1028. enum_val = enum_type(current_value)
  1029. combo.setCurrentText(f"{enum_val.name} ({current_value})")
  1030. except (ValueError, KeyError):
  1031. pass
  1032. layout.addWidget(combo)
  1033. buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
  1034. buttons.accepted.connect(dialog.accept)
  1035. buttons.rejected.connect(dialog.reject)
  1036. layout.addWidget(buttons)
  1037. if dialog.exec() == QDialog.DialogCode.Accepted:
  1038. # Get the selected value
  1039. new_value = combo.currentData()
  1040. enum_val = enum_type(new_value)
  1041. # Store the change
  1042. self.metadata_changes[key] = (field.types[0], new_value)
  1043. self.modified = True
  1044. # Update display
  1045. display_text = f"{enum_val.name} ({new_value})"
  1046. target_item = self.metadata_table.item(row, 2)
  1047. if target_item:
  1048. target_item.setText(display_text)
  1049. self.statusBar().showMessage(f"Changed {key} to {display_text}")
  1050. def edit_array_metadata(self):
  1051. button = self.sender()
  1052. key = button.property("key")
  1053. row = button.property("row")
  1054. # Check if this is one of the linked tokenizer keys
  1055. if key in TOKENIZER_LINKED_KEYS:
  1056. self.edit_tokenizer_metadata(key)
  1057. return
  1058. field = None
  1059. if self.reader:
  1060. field = self.reader.get_field(key)
  1061. if not field or not field.types or field.types[0] != GGUFValueType.ARRAY:
  1062. return
  1063. # Get array element type
  1064. element_type = field.types[1]
  1065. # Extract array values
  1066. array_values = self.extract_array_values(field)
  1067. # Open array editor dialog
  1068. dialog = ArrayEditorDialog(array_values, element_type, key, self)
  1069. if dialog.exec() == QDialog.DialogCode.Accepted:
  1070. new_values = dialog.get_array_values()
  1071. # Store the change
  1072. self.metadata_changes[key] = (GGUFValueType.ARRAY, (element_type, new_values))
  1073. self.modified = True
  1074. # Update display
  1075. enum_type = self.get_enum_for_key(key)
  1076. if enum_type is not None and element_type == GGUFValueType.INT32:
  1077. value_str = f"[ {', '.join(self.format_enum_value(v, enum_type) for v in new_values[:5])}{', ...' if len(new_values) > 5 else ''} ]"
  1078. else:
  1079. value_str = f"[ {', '.join(str(v) for v in new_values[:5])}{', ...' if len(new_values) > 5 else ''} ]"
  1080. target_item = self.metadata_table.item(row, 2)
  1081. if target_item:
  1082. target_item.setText(value_str)
  1083. self.statusBar().showMessage(f"Updated array values for {key}")
  1084. def edit_tokenizer_metadata(self, trigger_key):
  1085. """Edit the linked tokenizer metadata arrays together."""
  1086. if not self.reader:
  1087. return
  1088. # Get all three fields
  1089. tokens_field = self.reader.get_field(gguf.Keys.Tokenizer.LIST)
  1090. token_types_field = self.reader.get_field(gguf.Keys.Tokenizer.TOKEN_TYPE)
  1091. scores_field = self.reader.get_field(gguf.Keys.Tokenizer.SCORES)
  1092. # Extract values from each field
  1093. tokens = self.extract_array_values(tokens_field) if tokens_field else []
  1094. token_types = self.extract_array_values(token_types_field) if token_types_field else []
  1095. scores = self.extract_array_values(scores_field) if scores_field else []
  1096. # Apply any pending changes
  1097. if gguf.Keys.Tokenizer.LIST in self.metadata_changes:
  1098. _, (_, tokens) = self.metadata_changes[gguf.Keys.Tokenizer.LIST]
  1099. if gguf.Keys.Tokenizer.TOKEN_TYPE in self.metadata_changes:
  1100. _, (_, token_types) = self.metadata_changes[gguf.Keys.Tokenizer.TOKEN_TYPE]
  1101. if gguf.Keys.Tokenizer.SCORES in self.metadata_changes:
  1102. _, (_, scores) = self.metadata_changes[gguf.Keys.Tokenizer.SCORES]
  1103. # Open the tokenizer editor dialog
  1104. dialog = TokenizerEditorDialog(tokens, token_types, scores, self)
  1105. if dialog.exec() == QDialog.DialogCode.Accepted:
  1106. new_tokens, new_token_types, new_scores = dialog.get_data()
  1107. # Store changes for all three arrays
  1108. if tokens_field:
  1109. self.metadata_changes[gguf.Keys.Tokenizer.LIST] = (
  1110. GGUFValueType.ARRAY,
  1111. (tokens_field.types[1], new_tokens)
  1112. )
  1113. if token_types_field:
  1114. self.metadata_changes[gguf.Keys.Tokenizer.TOKEN_TYPE] = (
  1115. GGUFValueType.ARRAY,
  1116. (token_types_field.types[1], new_token_types)
  1117. )
  1118. if scores_field:
  1119. self.metadata_changes[gguf.Keys.Tokenizer.SCORES] = (
  1120. GGUFValueType.ARRAY,
  1121. (scores_field.types[1], new_scores)
  1122. )
  1123. self.modified = True
  1124. # Update display for all three fields
  1125. self.update_tokenizer_display(gguf.Keys.Tokenizer.LIST, new_tokens)
  1126. self.update_tokenizer_display(gguf.Keys.Tokenizer.TOKEN_TYPE, new_token_types)
  1127. self.update_tokenizer_display(gguf.Keys.Tokenizer.SCORES, new_scores)
  1128. self.statusBar().showMessage("Updated tokenizer data")
  1129. def update_tokenizer_display(self, key, values):
  1130. """Update the display of a tokenizer field in the metadata table."""
  1131. for row in range(self.metadata_table.rowCount()):
  1132. key_item = self.metadata_table.item(row, 0)
  1133. if key_item and key_item.text() == key:
  1134. value_str = f"[ {', '.join(str(v) for v in values[:5])}{', ...' if len(values) > 5 else ''} ]"
  1135. value_item = self.metadata_table.item(row, 2)
  1136. if value_item:
  1137. value_item.setText(value_str)
  1138. break
  1139. def add_metadata(self):
  1140. dialog = AddMetadataDialog(self)
  1141. if dialog.exec() == QDialog.DialogCode.Accepted:
  1142. key, value_type, value = dialog.get_data()
  1143. if not key:
  1144. QMessageBox.warning(self, "Invalid Key", "Key cannot be empty")
  1145. return
  1146. # Check if key already exists
  1147. for row in range(self.metadata_table.rowCount()):
  1148. orig_item = self.metadata_table.item(row, 0)
  1149. if orig_item and orig_item.text() == key:
  1150. QMessageBox.warning(self, "Duplicate Key", f"Key '{key}' already exists")
  1151. return
  1152. # Add to table
  1153. row = self.metadata_table.rowCount()
  1154. self.metadata_table.insertRow(row)
  1155. # Key
  1156. key_item = QTableWidgetItem(key)
  1157. key_item.setFlags(key_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  1158. self.metadata_table.setItem(row, 0, key_item)
  1159. # Type
  1160. type_item = QTableWidgetItem(value_type.name)
  1161. type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  1162. self.metadata_table.setItem(row, 1, type_item)
  1163. # Value
  1164. value_item = QTableWidgetItem(str(value))
  1165. value_item.setFlags(value_item.flags() | Qt.ItemFlag.ItemIsEditable)
  1166. self.metadata_table.setItem(row, 2, value_item)
  1167. # Actions
  1168. actions_widget = QWidget()
  1169. actions_layout = QHBoxLayout(actions_widget)
  1170. actions_layout.setContentsMargins(2, 2, 2, 2)
  1171. remove_button = QPushButton("Remove")
  1172. remove_button.setProperty("row", row)
  1173. remove_button.setProperty("key", key)
  1174. remove_button.clicked.connect(self.remove_metadata)
  1175. actions_layout.addWidget(remove_button)
  1176. self.metadata_table.setCellWidget(row, 3, actions_widget)
  1177. # Store the change
  1178. self.metadata_changes[key] = (value_type, value)
  1179. self.modified = True
  1180. self.statusBar().showMessage(f"Added new metadata key {key}")
  1181. def save_file(self):
  1182. if not self.reader:
  1183. QMessageBox.warning(self, "No File Open", "Please open a GGUF file first")
  1184. return
  1185. if not self.modified and not self.metadata_changes and not self.metadata_to_remove:
  1186. QMessageBox.information(self, "No Changes", "No changes to save")
  1187. return
  1188. file_path, _ = QFileDialog.getSaveFileName(
  1189. self, "Save GGUF File As", "", "GGUF Files (*.gguf);;All Files (*)"
  1190. )
  1191. if not file_path:
  1192. return
  1193. try:
  1194. self.statusBar().showMessage(f"Saving to {file_path}...")
  1195. QApplication.processEvents()
  1196. # Get architecture and endianness from the original file
  1197. arch = 'unknown'
  1198. field = self.reader.get_field(gguf.Keys.General.ARCHITECTURE)
  1199. if field:
  1200. arch = field.contents()
  1201. # Create writer
  1202. writer = GGUFWriter(file_path, arch=arch, endianess=self.reader.endianess)
  1203. # Get alignment if present
  1204. alignment = None
  1205. field = self.reader.get_field(gguf.Keys.General.ALIGNMENT)
  1206. if field:
  1207. alignment = field.contents()
  1208. if alignment is not None:
  1209. writer.data_alignment = alignment
  1210. # Copy metadata with changes
  1211. for field in self.reader.fields.values():
  1212. # Skip virtual fields and fields written by GGUFWriter
  1213. if field.name == gguf.Keys.General.ARCHITECTURE or field.name.startswith('GGUF.'):
  1214. continue
  1215. # Skip fields marked for removal
  1216. if field.name in self.metadata_to_remove:
  1217. continue
  1218. # Apply changes if any
  1219. sub_type = None
  1220. if field.name in self.metadata_changes:
  1221. value_type, value = self.metadata_changes[field.name]
  1222. if value_type == GGUFValueType.ARRAY:
  1223. # Handle array values
  1224. sub_type, value = value
  1225. else:
  1226. # Copy original value
  1227. value = field.contents()
  1228. value_type = field.types[0]
  1229. if value_type == GGUFValueType.ARRAY:
  1230. sub_type = field.types[-1]
  1231. if value is not None:
  1232. writer.add_key_value(field.name, value, value_type, sub_type=sub_type)
  1233. # Add new metadata
  1234. for key, (value_type, value) in self.metadata_changes.items():
  1235. # Skip if the key already existed (we handled it above)
  1236. if self.reader.get_field(key) is not None:
  1237. continue
  1238. sub_type = None
  1239. if value_type == GGUFValueType.ARRAY:
  1240. # Handle array values
  1241. sub_type, value = value
  1242. writer.add_key_value(key, value, value_type, sub_type=sub_type)
  1243. # Add tensors (including data)
  1244. for tensor in self.reader.tensors:
  1245. writer.add_tensor(tensor.name, tensor.data, raw_shape=tensor.data.shape, raw_dtype=tensor.tensor_type, tensor_endianess=self.reader.endianess)
  1246. # Write header and metadata
  1247. writer.open_output_file(Path(file_path))
  1248. writer.write_header_to_file()
  1249. writer.write_kv_data_to_file()
  1250. # Write tensor data using the optimized method
  1251. writer.write_tensors_to_file(progress=False)
  1252. writer.close()
  1253. self.statusBar().showMessage(f"Saved to {file_path}")
  1254. # Ask if user wants to open the new file
  1255. reply = QMessageBox.question(
  1256. self, "Open Saved File",
  1257. "Would you like to open the newly saved file?",
  1258. QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.Yes
  1259. )
  1260. if reply == QMessageBox.StandardButton.Yes:
  1261. self.reader = GGUFReader(file_path, 'r')
  1262. self.current_file = file_path
  1263. self.file_path_edit.setText(file_path)
  1264. self.load_metadata()
  1265. self.load_tensors()
  1266. self.metadata_changes = {}
  1267. self.metadata_to_remove = set()
  1268. self.modified = False
  1269. except Exception as e:
  1270. QMessageBox.critical(self, "Error", f"Failed to save file: {str(e)}")
  1271. self.statusBar().showMessage("Error saving file")
  1272. def main() -> None:
  1273. parser = argparse.ArgumentParser(description="GUI GGUF Editor")
  1274. parser.add_argument("model_path", nargs="?", help="path to GGUF model file to load at startup")
  1275. parser.add_argument("--verbose", action="store_true", help="increase output verbosity")
  1276. args = parser.parse_args()
  1277. logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
  1278. app = QApplication(sys.argv)
  1279. window = GGUFEditorWindow()
  1280. window.show()
  1281. # Load model if specified
  1282. if args.model_path:
  1283. if os.path.isfile(args.model_path) and args.model_path.endswith('.gguf'):
  1284. window.load_file(args.model_path)
  1285. else:
  1286. logger.error(f"Invalid model path: {args.model_path}")
  1287. QMessageBox.warning(
  1288. window,
  1289. "Invalid Model Path",
  1290. f"The specified file does not exist or is not a GGUF file: {args.model_path}")
  1291. sys.exit(app.exec())
  1292. if __name__ == '__main__':
  1293. main()