gguf_editor_gui.py 62 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610
  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.setup_ui()
  663. def setup_ui(self):
  664. central_widget = QWidget()
  665. self.setCentralWidget(central_widget)
  666. main_layout = QVBoxLayout(central_widget)
  667. # File controls
  668. file_layout = QHBoxLayout()
  669. self.file_path_edit = QLineEdit()
  670. self.file_path_edit.setReadOnly(True)
  671. file_layout.addWidget(self.file_path_edit)
  672. open_button = QPushButton("Open GGUF")
  673. open_button.clicked.connect(self.open_file)
  674. file_layout.addWidget(open_button)
  675. save_button = QPushButton("Save As...")
  676. save_button.clicked.connect(self.save_file)
  677. file_layout.addWidget(save_button)
  678. main_layout.addLayout(file_layout)
  679. # Tabs for different views
  680. self.tabs = QTabWidget()
  681. # Metadata tab
  682. self.metadata_tab = QWidget()
  683. metadata_layout = QVBoxLayout(self.metadata_tab)
  684. # Metadata table
  685. self.metadata_table = QTableWidget()
  686. self.metadata_table.setColumnCount(4)
  687. self.metadata_table.setHorizontalHeaderLabels(["Key", "Type", "Value", "Actions"])
  688. self.metadata_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
  689. self.metadata_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
  690. self.metadata_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
  691. self.metadata_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
  692. metadata_layout.addWidget(self.metadata_table)
  693. # Metadata controls
  694. metadata_controls = QHBoxLayout()
  695. add_metadata_button = QPushButton("Add Metadata")
  696. add_metadata_button.clicked.connect(self.add_metadata)
  697. metadata_controls.addWidget(add_metadata_button)
  698. metadata_controls.addStretch()
  699. metadata_layout.addLayout(metadata_controls)
  700. # Tensors tab
  701. self.tensors_tab = QWidget()
  702. tensors_layout = QVBoxLayout(self.tensors_tab)
  703. self.tensors_table = QTableWidget()
  704. self.tensors_table.setColumnCount(5)
  705. self.tensors_table.setHorizontalHeaderLabels(["Name", "Type", "Shape", "Elements", "Size (bytes)"])
  706. self.tensors_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
  707. self.tensors_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
  708. self.tensors_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
  709. self.tensors_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
  710. self.tensors_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents)
  711. tensors_layout.addWidget(self.tensors_table)
  712. # Add tabs to tab widget
  713. self.tabs.addTab(self.metadata_tab, "Metadata")
  714. self.tabs.addTab(self.tensors_tab, "Tensors")
  715. main_layout.addWidget(self.tabs)
  716. # Status bar
  717. self.statusBar().showMessage("Ready")
  718. def load_file(self, file_path):
  719. """Load a GGUF file by path"""
  720. try:
  721. self.statusBar().showMessage(f"Loading {file_path}...")
  722. QApplication.processEvents()
  723. self.reader = GGUFReader(file_path, 'r')
  724. self.current_file = file_path
  725. self.file_path_edit.setText(file_path)
  726. self.load_metadata()
  727. self.load_tensors()
  728. self.metadata_changes = {}
  729. self.metadata_to_remove = set()
  730. self.modified = False
  731. self.statusBar().showMessage(f"Loaded {file_path}")
  732. return True
  733. except Exception as e:
  734. QMessageBox.critical(self, "Error", f"Failed to open file: {str(e)}")
  735. self.statusBar().showMessage("Error loading file")
  736. return False
  737. def open_file(self):
  738. file_path, _ = QFileDialog.getOpenFileName(
  739. self, "Open GGUF File", "", "GGUF Files (*.gguf);;All Files (*)"
  740. )
  741. if not file_path:
  742. return
  743. self.load_file(file_path)
  744. def load_metadata(self):
  745. self.metadata_table.setRowCount(0)
  746. if not self.reader:
  747. return
  748. # Disconnect to prevent triggering during loading
  749. with warnings.catch_warnings():
  750. warnings.filterwarnings('ignore')
  751. self.metadata_table.itemChanged.disconnect(self.on_metadata_changed)
  752. for i, (key, field) in enumerate(self.reader.fields.items()):
  753. self.metadata_table.insertRow(i)
  754. # Key
  755. key_item = QTableWidgetItem(key)
  756. key_item.setFlags(key_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  757. self.metadata_table.setItem(i, 0, key_item)
  758. # Type
  759. if not field.types:
  760. type_str = "N/A"
  761. elif field.types[0] == GGUFValueType.ARRAY:
  762. nest_count = len(field.types) - 1
  763. element_type = field.types[-1].name
  764. # Check if this is an enum array
  765. enum_type = self.get_enum_for_key(key)
  766. if enum_type is not None and field.types[-1] == GGUFValueType.INT32:
  767. element_type = enum_type.__name__
  768. type_str = '[' * nest_count + element_type + ']' * nest_count
  769. else:
  770. type_str = str(field.types[0].name)
  771. # Check if this is an enum field
  772. enum_type = self.get_enum_for_key(key)
  773. if enum_type is not None and field.types[0] == GGUFValueType.INT32:
  774. type_str = enum_type.__name__
  775. type_item = QTableWidgetItem(type_str)
  776. type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  777. self.metadata_table.setItem(i, 1, type_item)
  778. # Value
  779. value_str = self.format_field_value(field)
  780. value_item = QTableWidgetItem(value_str)
  781. # Make only simple values editable
  782. if len(field.types) == 1 and field.types[0] != GGUFValueType.ARRAY:
  783. value_item.setFlags(value_item.flags() | Qt.ItemFlag.ItemIsEditable)
  784. else:
  785. value_item.setFlags(value_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  786. self.metadata_table.setItem(i, 2, value_item)
  787. # Actions
  788. actions_widget = QWidget()
  789. actions_layout = QHBoxLayout(actions_widget)
  790. actions_layout.setContentsMargins(2, 2, 2, 2)
  791. # Add Edit button for arrays and enum fields
  792. if field.types and field.types[0] == GGUFValueType.ARRAY:
  793. edit_button = QPushButton("Edit")
  794. edit_button.setProperty("row", i)
  795. edit_button.setProperty("key", key)
  796. edit_button.clicked.connect(self.edit_array_metadata)
  797. actions_layout.addWidget(edit_button)
  798. # Add special label for tokenizer linked fields
  799. if key in TOKENIZER_LINKED_KEYS:
  800. edit_button.setText("Edit Tokenizer")
  801. edit_button.setToolTip("Edit all tokenizer data together")
  802. elif len(field.types) == 1 and self.get_enum_for_key(key) is not None:
  803. edit_button = QPushButton("Edit")
  804. edit_button.setProperty("row", i)
  805. edit_button.setProperty("key", key)
  806. edit_button.clicked.connect(self.edit_metadata_enum)
  807. actions_layout.addWidget(edit_button)
  808. remove_button = QPushButton("Remove")
  809. remove_button.setProperty("row", i)
  810. remove_button.setProperty("key", key)
  811. remove_button.clicked.connect(self.remove_metadata)
  812. actions_layout.addWidget(remove_button)
  813. self.metadata_table.setCellWidget(i, 3, actions_widget)
  814. # Reconnect after loading
  815. self.metadata_table.itemChanged.connect(self.on_metadata_changed)
  816. def extract_array_values(self, field: ReaderField) -> list:
  817. """Extract all values from an array field."""
  818. if not field.types or field.types[0] != GGUFValueType.ARRAY:
  819. return []
  820. curr_type = field.types[1]
  821. array_values = []
  822. total_elements = len(field.data)
  823. if curr_type == GGUFValueType.STRING:
  824. for element_pos in range(total_elements):
  825. value_string = str(bytes(field.parts[-1 - (total_elements - element_pos - 1) * 2]), encoding='utf-8')
  826. array_values.append(value_string)
  827. elif self.reader and curr_type in self.reader.gguf_scalar_to_np:
  828. for element_pos in range(total_elements):
  829. array_values.append(field.parts[-1 - (total_elements - element_pos - 1)][0])
  830. return array_values
  831. def get_enum_for_key(self, key: str) -> Optional[Type[enum.Enum]]:
  832. """Get the enum type for a given key if it exists."""
  833. return KEY_TO_ENUM_TYPE.get(key)
  834. def format_enum_value(self, value: Any, enum_type: Type[enum.Enum]) -> str:
  835. """Format a value as an enum if possible."""
  836. try:
  837. if isinstance(value, (int, str)):
  838. enum_value = enum_type(value)
  839. return f"{enum_value.name} ({value})"
  840. except (ValueError, KeyError):
  841. pass
  842. return str(value)
  843. def format_field_value(self, field: ReaderField) -> str:
  844. if not field.types:
  845. return "N/A"
  846. if len(field.types) == 1:
  847. curr_type = field.types[0]
  848. if curr_type == GGUFValueType.STRING:
  849. return str(bytes(field.parts[-1]), encoding='utf-8')
  850. elif self.reader and curr_type in self.reader.gguf_scalar_to_np:
  851. value = field.parts[-1][0]
  852. # Check if this field has an enum type
  853. enum_type = self.get_enum_for_key(field.name)
  854. if enum_type is not None:
  855. return self.format_enum_value(value, enum_type)
  856. return str(value)
  857. if field.types[0] == GGUFValueType.ARRAY:
  858. array_values = self.extract_array_values(field)
  859. render_element = min(5, len(array_values))
  860. # Get enum type for this array if applicable
  861. enum_type = self.get_enum_for_key(field.name)
  862. if enum_type is not None:
  863. array_elements = []
  864. for i in range(render_element):
  865. array_elements.append(self.format_enum_value(array_values[i], enum_type))
  866. else:
  867. array_elements = [str(array_values[i]) for i in range(render_element)]
  868. return f"[ {', '.join(array_elements).strip()}{', ...' if len(array_values) > len(array_elements) else ''} ]"
  869. return "Complex value"
  870. def load_tensors(self):
  871. self.tensors_table.setRowCount(0)
  872. if not self.reader:
  873. return
  874. for i, tensor in enumerate(self.reader.tensors):
  875. self.tensors_table.insertRow(i)
  876. # Name
  877. name_item = QTableWidgetItem(tensor.name)
  878. name_item.setFlags(name_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  879. self.tensors_table.setItem(i, 0, name_item)
  880. # Type
  881. type_item = QTableWidgetItem(tensor.tensor_type.name)
  882. type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  883. self.tensors_table.setItem(i, 1, type_item)
  884. # Shape
  885. shape_str = " × ".join(str(d) for d in tensor.shape)
  886. shape_item = QTableWidgetItem(shape_str)
  887. shape_item.setFlags(shape_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  888. self.tensors_table.setItem(i, 2, shape_item)
  889. # Elements
  890. elements_item = QTableWidgetItem(str(tensor.n_elements))
  891. elements_item.setFlags(elements_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  892. self.tensors_table.setItem(i, 3, elements_item)
  893. # Size
  894. size_item = QTableWidgetItem(f"{tensor.n_bytes:,}")
  895. size_item.setFlags(size_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  896. self.tensors_table.setItem(i, 4, size_item)
  897. def on_metadata_changed(self, item):
  898. if item.column() != 2: # Only handle value column changes
  899. return
  900. row = item.row()
  901. orig_item = self.metadata_table.item(row, 0)
  902. key = None
  903. if orig_item:
  904. key = orig_item.text()
  905. new_value = item.text()
  906. field = None
  907. if self.reader and key:
  908. field = self.reader.get_field(key)
  909. if not field or not field.types or not key:
  910. return
  911. value_type = field.types[0]
  912. # Check if this is an enum field
  913. enum_type = self.get_enum_for_key(key)
  914. if enum_type is not None and value_type == GGUFValueType.INT32:
  915. # Try to parse the enum value from the text
  916. try:
  917. # Check if it's a name
  918. try:
  919. enum_val = enum_type[new_value]
  920. converted_value = enum_val.value
  921. except (KeyError, AttributeError):
  922. # Check if it's a number or "NAME (value)" format
  923. if '(' in new_value and ')' in new_value:
  924. # Extract the value from "NAME (value)" format
  925. value_part = new_value.split('(')[1].split(')')[0].strip()
  926. converted_value = int(value_part)
  927. else:
  928. # Try to convert directly to int
  929. converted_value = int(new_value)
  930. # Validate that it's a valid enum value
  931. enum_type(converted_value)
  932. # Store the change
  933. self.metadata_changes[key] = (value_type, converted_value)
  934. self.modified = True
  935. # Update display with formatted enum value
  936. formatted_value = self.format_enum_value(converted_value, enum_type)
  937. item.setText(formatted_value)
  938. self.statusBar().showMessage(f"Changed {key} to {formatted_value}")
  939. return
  940. except (ValueError, KeyError) as e:
  941. QMessageBox.warning(
  942. self,
  943. f"Invalid Enum Value ({e})",
  944. f"'{new_value}' is not a valid {enum_type.__name__} value.\n"
  945. f"Valid values are: {', '.join(v.name for v in enum_type)}")
  946. # Revert to original value
  947. original_value = self.format_field_value(field)
  948. item.setText(original_value)
  949. return
  950. try:
  951. # Convert the string value to the appropriate type
  952. if value_type == GGUFValueType.UINT8:
  953. converted_value = np.uint8(int(new_value))
  954. elif value_type == GGUFValueType.INT8:
  955. converted_value = np.int8(int(new_value))
  956. elif value_type == GGUFValueType.UINT16:
  957. converted_value = np.uint16(int(new_value))
  958. elif value_type == GGUFValueType.INT16:
  959. converted_value = np.int16(int(new_value))
  960. elif value_type == GGUFValueType.UINT32:
  961. converted_value = np.uint32(int(new_value))
  962. elif value_type == GGUFValueType.INT32:
  963. converted_value = np.int32(int(new_value))
  964. elif value_type == GGUFValueType.FLOAT32:
  965. converted_value = np.float32(float(new_value))
  966. elif value_type == GGUFValueType.BOOL:
  967. converted_value = new_value.lower() in ('true', 'yes', '1')
  968. elif value_type == GGUFValueType.STRING:
  969. converted_value = new_value
  970. else:
  971. # Unsupported type for editing
  972. return
  973. # Store the change
  974. self.metadata_changes[key] = (value_type, converted_value)
  975. self.modified = True
  976. self.statusBar().showMessage(f"Changed {key} to {new_value}")
  977. except ValueError:
  978. QMessageBox.warning(self, "Invalid Value", f"The value '{new_value}' is not valid for type {value_type.name}")
  979. # Revert to original value
  980. original_value = self.format_field_value(field)
  981. item.setText(original_value)
  982. def remove_metadata(self):
  983. button = self.sender()
  984. key = button.property("key")
  985. row = button.property("row")
  986. reply = QMessageBox.question(
  987. self, "Confirm Removal",
  988. f"Are you sure you want to remove the metadata key '{key}'?",
  989. QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No
  990. )
  991. if reply == QMessageBox.StandardButton.Yes:
  992. self.metadata_table.removeRow(row)
  993. self.metadata_to_remove.add(key)
  994. # If we previously had changes for this key, remove them
  995. if key in self.metadata_changes:
  996. del self.metadata_changes[key]
  997. self.modified = True
  998. self.statusBar().showMessage(f"Marked {key} for removal")
  999. def edit_metadata_enum(self):
  1000. """Edit an enum metadata field."""
  1001. button = self.sender()
  1002. key = button.property("key")
  1003. row = button.property("row")
  1004. field = None
  1005. if self.reader:
  1006. field = self.reader.get_field(key)
  1007. if not field or not field.types:
  1008. return
  1009. enum_type = self.get_enum_for_key(key)
  1010. if enum_type is None:
  1011. return
  1012. # Get current value
  1013. current_value = field.contents()
  1014. # Create a dialog with enum options
  1015. dialog = QDialog(self)
  1016. dialog.setWindowTitle(f"Select {enum_type.__name__} Value")
  1017. layout = QVBoxLayout(dialog)
  1018. combo = QComboBox()
  1019. for enum_val in enum_type:
  1020. combo.addItem(f"{enum_val.name} ({enum_val.value})", enum_val.value)
  1021. # Set current value
  1022. try:
  1023. if isinstance(current_value, (int, str)):
  1024. enum_val = enum_type(current_value)
  1025. combo.setCurrentText(f"{enum_val.name} ({current_value})")
  1026. except (ValueError, KeyError):
  1027. pass
  1028. layout.addWidget(combo)
  1029. buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
  1030. buttons.accepted.connect(dialog.accept)
  1031. buttons.rejected.connect(dialog.reject)
  1032. layout.addWidget(buttons)
  1033. if dialog.exec() == QDialog.DialogCode.Accepted:
  1034. # Get the selected value
  1035. new_value = combo.currentData()
  1036. enum_val = enum_type(new_value)
  1037. # Store the change
  1038. self.metadata_changes[key] = (field.types[0], new_value)
  1039. self.modified = True
  1040. # Update display
  1041. display_text = f"{enum_val.name} ({new_value})"
  1042. target_item = self.metadata_table.item(row, 2)
  1043. if target_item:
  1044. target_item.setText(display_text)
  1045. self.statusBar().showMessage(f"Changed {key} to {display_text}")
  1046. def edit_array_metadata(self):
  1047. button = self.sender()
  1048. key = button.property("key")
  1049. row = button.property("row")
  1050. # Check if this is one of the linked tokenizer keys
  1051. if key in TOKENIZER_LINKED_KEYS:
  1052. self.edit_tokenizer_metadata(key)
  1053. return
  1054. field = None
  1055. if self.reader:
  1056. field = self.reader.get_field(key)
  1057. if not field or not field.types or field.types[0] != GGUFValueType.ARRAY:
  1058. return
  1059. # Get array element type
  1060. element_type = field.types[1]
  1061. # Extract array values
  1062. array_values = self.extract_array_values(field)
  1063. # Open array editor dialog
  1064. dialog = ArrayEditorDialog(array_values, element_type, key, self)
  1065. if dialog.exec() == QDialog.DialogCode.Accepted:
  1066. new_values = dialog.get_array_values()
  1067. # Store the change
  1068. self.metadata_changes[key] = (GGUFValueType.ARRAY, (element_type, new_values))
  1069. self.modified = True
  1070. # Update display
  1071. enum_type = self.get_enum_for_key(key)
  1072. if enum_type is not None and element_type == GGUFValueType.INT32:
  1073. value_str = f"[ {', '.join(self.format_enum_value(v, enum_type) for v in new_values[:5])}{', ...' if len(new_values) > 5 else ''} ]"
  1074. else:
  1075. value_str = f"[ {', '.join(str(v) for v in new_values[:5])}{', ...' if len(new_values) > 5 else ''} ]"
  1076. target_item = self.metadata_table.item(row, 2)
  1077. if target_item:
  1078. target_item.setText(value_str)
  1079. self.statusBar().showMessage(f"Updated array values for {key}")
  1080. def edit_tokenizer_metadata(self, trigger_key):
  1081. """Edit the linked tokenizer metadata arrays together."""
  1082. if not self.reader:
  1083. return
  1084. # Get all three fields
  1085. tokens_field = self.reader.get_field(gguf.Keys.Tokenizer.LIST)
  1086. token_types_field = self.reader.get_field(gguf.Keys.Tokenizer.TOKEN_TYPE)
  1087. scores_field = self.reader.get_field(gguf.Keys.Tokenizer.SCORES)
  1088. # Extract values from each field
  1089. tokens = self.extract_array_values(tokens_field) if tokens_field else []
  1090. token_types = self.extract_array_values(token_types_field) if token_types_field else []
  1091. scores = self.extract_array_values(scores_field) if scores_field else []
  1092. # Apply any pending changes
  1093. if gguf.Keys.Tokenizer.LIST in self.metadata_changes:
  1094. _, (_, tokens) = self.metadata_changes[gguf.Keys.Tokenizer.LIST]
  1095. if gguf.Keys.Tokenizer.TOKEN_TYPE in self.metadata_changes:
  1096. _, (_, token_types) = self.metadata_changes[gguf.Keys.Tokenizer.TOKEN_TYPE]
  1097. if gguf.Keys.Tokenizer.SCORES in self.metadata_changes:
  1098. _, (_, scores) = self.metadata_changes[gguf.Keys.Tokenizer.SCORES]
  1099. # Open the tokenizer editor dialog
  1100. dialog = TokenizerEditorDialog(tokens, token_types, scores, self)
  1101. if dialog.exec() == QDialog.DialogCode.Accepted:
  1102. new_tokens, new_token_types, new_scores = dialog.get_data()
  1103. # Store changes for all three arrays
  1104. if tokens_field:
  1105. self.metadata_changes[gguf.Keys.Tokenizer.LIST] = (
  1106. GGUFValueType.ARRAY,
  1107. (tokens_field.types[1], new_tokens)
  1108. )
  1109. if token_types_field:
  1110. self.metadata_changes[gguf.Keys.Tokenizer.TOKEN_TYPE] = (
  1111. GGUFValueType.ARRAY,
  1112. (token_types_field.types[1], new_token_types)
  1113. )
  1114. if scores_field:
  1115. self.metadata_changes[gguf.Keys.Tokenizer.SCORES] = (
  1116. GGUFValueType.ARRAY,
  1117. (scores_field.types[1], new_scores)
  1118. )
  1119. self.modified = True
  1120. # Update display for all three fields
  1121. self.update_tokenizer_display(gguf.Keys.Tokenizer.LIST, new_tokens)
  1122. self.update_tokenizer_display(gguf.Keys.Tokenizer.TOKEN_TYPE, new_token_types)
  1123. self.update_tokenizer_display(gguf.Keys.Tokenizer.SCORES, new_scores)
  1124. self.statusBar().showMessage("Updated tokenizer data")
  1125. def update_tokenizer_display(self, key, values):
  1126. """Update the display of a tokenizer field in the metadata table."""
  1127. for row in range(self.metadata_table.rowCount()):
  1128. key_item = self.metadata_table.item(row, 0)
  1129. if key_item and key_item.text() == key:
  1130. value_str = f"[ {', '.join(str(v) for v in values[:5])}{', ...' if len(values) > 5 else ''} ]"
  1131. value_item = self.metadata_table.item(row, 2)
  1132. if value_item:
  1133. value_item.setText(value_str)
  1134. break
  1135. def add_metadata(self):
  1136. dialog = AddMetadataDialog(self)
  1137. if dialog.exec() == QDialog.DialogCode.Accepted:
  1138. key, value_type, value = dialog.get_data()
  1139. if not key:
  1140. QMessageBox.warning(self, "Invalid Key", "Key cannot be empty")
  1141. return
  1142. # Check if key already exists
  1143. for row in range(self.metadata_table.rowCount()):
  1144. orig_item = self.metadata_table.item(row, 0)
  1145. if orig_item and orig_item.text() == key:
  1146. QMessageBox.warning(self, "Duplicate Key", f"Key '{key}' already exists")
  1147. return
  1148. # Add to table
  1149. row = self.metadata_table.rowCount()
  1150. self.metadata_table.insertRow(row)
  1151. # Key
  1152. key_item = QTableWidgetItem(key)
  1153. key_item.setFlags(key_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  1154. self.metadata_table.setItem(row, 0, key_item)
  1155. # Type
  1156. type_item = QTableWidgetItem(value_type.name)
  1157. type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
  1158. self.metadata_table.setItem(row, 1, type_item)
  1159. # Value
  1160. value_item = QTableWidgetItem(str(value))
  1161. value_item.setFlags(value_item.flags() | Qt.ItemFlag.ItemIsEditable)
  1162. self.metadata_table.setItem(row, 2, value_item)
  1163. # Actions
  1164. actions_widget = QWidget()
  1165. actions_layout = QHBoxLayout(actions_widget)
  1166. actions_layout.setContentsMargins(2, 2, 2, 2)
  1167. remove_button = QPushButton("Remove")
  1168. remove_button.setProperty("row", row)
  1169. remove_button.setProperty("key", key)
  1170. remove_button.clicked.connect(self.remove_metadata)
  1171. actions_layout.addWidget(remove_button)
  1172. self.metadata_table.setCellWidget(row, 3, actions_widget)
  1173. # Store the change
  1174. self.metadata_changes[key] = (value_type, value)
  1175. self.modified = True
  1176. self.statusBar().showMessage(f"Added new metadata key {key}")
  1177. def save_file(self):
  1178. if not self.reader:
  1179. QMessageBox.warning(self, "No File Open", "Please open a GGUF file first")
  1180. return
  1181. if not self.modified and not self.metadata_changes and not self.metadata_to_remove:
  1182. QMessageBox.information(self, "No Changes", "No changes to save")
  1183. return
  1184. file_path, _ = QFileDialog.getSaveFileName(
  1185. self, "Save GGUF File As", "", "GGUF Files (*.gguf);;All Files (*)"
  1186. )
  1187. if not file_path:
  1188. return
  1189. try:
  1190. self.statusBar().showMessage(f"Saving to {file_path}...")
  1191. QApplication.processEvents()
  1192. # Get architecture and endianness from the original file
  1193. arch = 'unknown'
  1194. field = self.reader.get_field(gguf.Keys.General.ARCHITECTURE)
  1195. if field:
  1196. arch = field.contents()
  1197. # Create writer
  1198. writer = GGUFWriter(file_path, arch=arch, endianess=self.reader.endianess)
  1199. # Get alignment if present
  1200. alignment = None
  1201. field = self.reader.get_field(gguf.Keys.General.ALIGNMENT)
  1202. if field:
  1203. alignment = field.contents()
  1204. if alignment is not None:
  1205. writer.data_alignment = alignment
  1206. # Copy metadata with changes
  1207. for field in self.reader.fields.values():
  1208. # Skip virtual fields and fields written by GGUFWriter
  1209. if field.name == gguf.Keys.General.ARCHITECTURE or field.name.startswith('GGUF.'):
  1210. continue
  1211. # Skip fields marked for removal
  1212. if field.name in self.metadata_to_remove:
  1213. continue
  1214. # Apply changes if any
  1215. if field.name in self.metadata_changes:
  1216. value_type, value = self.metadata_changes[field.name]
  1217. if value_type == GGUFValueType.ARRAY:
  1218. # Handle array values
  1219. element_type, array_values = value
  1220. writer.add_array(field.name, array_values)
  1221. else:
  1222. writer.add_key_value(field.name, value, value_type)
  1223. else:
  1224. # Copy original value
  1225. value = field.contents()
  1226. if value is not None and field.types:
  1227. writer.add_key_value(field.name, value, field.types[0])
  1228. # Add new metadata
  1229. for key, (value_type, value) in self.metadata_changes.items():
  1230. # Skip if the key already existed (we handled it above)
  1231. if self.reader.get_field(key) is not None:
  1232. continue
  1233. writer.add_key_value(key, value, value_type)
  1234. # Add tensors (including data)
  1235. for tensor in self.reader.tensors:
  1236. writer.add_tensor(tensor.name, tensor.data, raw_shape=tensor.data.shape, raw_dtype=tensor.tensor_type)
  1237. # Write header and metadata
  1238. writer.open_output_file(Path(file_path))
  1239. writer.write_header_to_file()
  1240. writer.write_kv_data_to_file()
  1241. # Write tensor data using the optimized method
  1242. writer.write_tensors_to_file(progress=False)
  1243. writer.close()
  1244. self.statusBar().showMessage(f"Saved to {file_path}")
  1245. # Ask if user wants to open the new file
  1246. reply = QMessageBox.question(
  1247. self, "Open Saved File",
  1248. "Would you like to open the newly saved file?",
  1249. QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.Yes
  1250. )
  1251. if reply == QMessageBox.StandardButton.Yes:
  1252. self.reader = GGUFReader(file_path, 'r')
  1253. self.current_file = file_path
  1254. self.file_path_edit.setText(file_path)
  1255. self.load_metadata()
  1256. self.load_tensors()
  1257. self.metadata_changes = {}
  1258. self.metadata_to_remove = set()
  1259. self.modified = False
  1260. except Exception as e:
  1261. QMessageBox.critical(self, "Error", f"Failed to save file: {str(e)}")
  1262. self.statusBar().showMessage("Error saving file")
  1263. def main() -> None:
  1264. parser = argparse.ArgumentParser(description="GUI GGUF Editor")
  1265. parser.add_argument("model_path", nargs="?", help="path to GGUF model file to load at startup")
  1266. parser.add_argument("--verbose", action="store_true", help="increase output verbosity")
  1267. args = parser.parse_args()
  1268. logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
  1269. app = QApplication(sys.argv)
  1270. window = GGUFEditorWindow()
  1271. window.show()
  1272. # Load model if specified
  1273. if args.model_path:
  1274. if os.path.isfile(args.model_path) and args.model_path.endswith('.gguf'):
  1275. window.load_file(args.model_path)
  1276. else:
  1277. logger.error(f"Invalid model path: {args.model_path}")
  1278. QMessageBox.warning(
  1279. window,
  1280. "Invalid Model Path",
  1281. f"The specified file does not exist or is not a GGUF file: {args.model_path}")
  1282. sys.exit(app.exec())
  1283. if __name__ == '__main__':
  1284. main()