| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621 |
- #!/usr/bin/env python3
- from __future__ import annotations
- import logging
- import argparse
- import os
- import sys
- import numpy
- import enum
- from pathlib import Path
- from typing import Any, Optional, Tuple, Type
- import warnings
- import numpy as np
- from PySide6.QtWidgets import (
- QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
- QPushButton, QLabel, QLineEdit, QFileDialog, QTableWidget,
- QTableWidgetItem, QComboBox, QMessageBox, QTabWidget,
- QTextEdit, QFormLayout,
- QHeaderView, QDialog, QDialogButtonBox
- )
- from PySide6.QtCore import Qt
- # Necessary to load the local gguf package
- if "NO_LOCAL_GGUF" not in os.environ and (Path(__file__).parent.parent.parent.parent / 'gguf-py').exists():
- sys.path.insert(0, str(Path(__file__).parent.parent.parent))
- import gguf
- from gguf import GGUFReader, GGUFWriter, GGUFValueType, ReaderField
- from gguf.constants import TokenType, RopeScalingType, PoolingType, GGMLQuantizationType
- logger = logging.getLogger("gguf-editor-gui")
- # Map of key names to enum types for automatic enum interpretation
- KEY_TO_ENUM_TYPE = {
- gguf.Keys.Tokenizer.TOKEN_TYPE: TokenType,
- gguf.Keys.Rope.SCALING_TYPE: RopeScalingType,
- gguf.Keys.LLM.POOLING_TYPE: PoolingType,
- gguf.Keys.General.FILE_TYPE: GGMLQuantizationType,
- }
- # Define the tokenizer keys that should be edited together
- TOKENIZER_LINKED_KEYS = [
- gguf.Keys.Tokenizer.LIST,
- gguf.Keys.Tokenizer.TOKEN_TYPE,
- gguf.Keys.Tokenizer.SCORES
- ]
- class TokenizerEditorDialog(QDialog):
- def __init__(self, tokens, token_types, scores, parent=None):
- super().__init__(parent)
- self.setWindowTitle("Edit Tokenizer Data")
- self.resize(900, 600)
- self.tokens = tokens.copy() if tokens else []
- self.token_types = token_types.copy() if token_types else []
- self.scores = scores.copy() if scores else []
- # Ensure all arrays have the same length
- max_len = max(len(self.tokens), len(self.token_types), len(self.scores))
- if len(self.tokens) < max_len:
- self.tokens.extend([""] * (max_len - len(self.tokens)))
- if len(self.token_types) < max_len:
- self.token_types.extend([0] * (max_len - len(self.token_types)))
- if len(self.scores) < max_len:
- self.scores.extend([0.0] * (max_len - len(self.scores)))
- layout = QVBoxLayout(self)
- # Add filter controls
- filter_layout = QHBoxLayout()
- filter_layout.addWidget(QLabel("Filter:"))
- self.filter_edit = QLineEdit()
- self.filter_edit.setPlaceholderText("Type to filter tokens...")
- self.filter_edit.textChanged.connect(self.apply_filter)
- filter_layout.addWidget(self.filter_edit)
- # Add page controls
- self.page_size = 100 # Show 100 items per page
- self.current_page = 0
- self.total_pages = max(1, (len(self.tokens) + self.page_size - 1) // self.page_size)
- self.page_label = QLabel(f"Page 1 of {self.total_pages}")
- filter_layout.addWidget(self.page_label)
- prev_page = QPushButton("Previous")
- prev_page.clicked.connect(self.previous_page)
- filter_layout.addWidget(prev_page)
- next_page = QPushButton("Next")
- next_page.clicked.connect(self.next_page)
- filter_layout.addWidget(next_page)
- layout.addLayout(filter_layout)
- # Tokenizer data table
- self.tokens_table = QTableWidget()
- self.tokens_table.setColumnCount(4)
- self.tokens_table.setHorizontalHeaderLabels(["Index", "Token", "Type", "Score"])
- self.tokens_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
- self.tokens_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
- self.tokens_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
- self.tokens_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
- layout.addWidget(self.tokens_table)
- # Controls
- controls_layout = QHBoxLayout()
- add_button = QPushButton("Add Token")
- add_button.clicked.connect(self.add_token)
- controls_layout.addWidget(add_button)
- remove_button = QPushButton("Remove Selected")
- remove_button.clicked.connect(self.remove_selected)
- controls_layout.addWidget(remove_button)
- controls_layout.addStretch()
- layout.addLayout(controls_layout)
- # Buttons
- buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
- buttons.accepted.connect(self.accept)
- buttons.rejected.connect(self.reject)
- layout.addWidget(buttons)
- # Initialize the filtered values
- self.filtered_indices = list(range(len(self.tokens)))
- # Load data for the first page
- self.load_page()
- def apply_filter(self):
- """Filter the tokens based on the search text."""
- filter_text = self.filter_edit.text().lower()
- if not filter_text:
- # No filter, show all values
- self.filtered_indices = list(range(len(self.tokens)))
- else:
- # Apply filter
- self.filtered_indices = []
- for i, token in enumerate(self.tokens):
- if filter_text in str(token).lower():
- self.filtered_indices.append(i)
- # Reset to first page and reload
- self.total_pages = max(1, (len(self.filtered_indices) + self.page_size - 1) // self.page_size)
- self.current_page = 0
- self.page_label.setText(f"Page 1 of {self.total_pages}")
- self.load_page()
- def previous_page(self):
- """Go to the previous page of results."""
- if self.current_page > 0:
- self.current_page -= 1
- self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
- self.load_page()
- def next_page(self):
- """Go to the next page of results."""
- if self.current_page < self.total_pages - 1:
- self.current_page += 1
- self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
- self.load_page()
- def load_page(self):
- """Load the current page of tokenizer data."""
- self.tokens_table.setRowCount(0) # Clear the table
- # Calculate start and end indices for the current page
- start_idx = self.current_page * self.page_size
- end_idx = min(start_idx + self.page_size, len(self.filtered_indices))
- # Pre-allocate rows for better performance
- self.tokens_table.setRowCount(end_idx - start_idx)
- for row, i in enumerate(range(start_idx, end_idx)):
- orig_idx = self.filtered_indices[i]
- # Index
- index_item = QTableWidgetItem(str(orig_idx))
- index_item.setData(Qt.ItemDataRole.UserRole, orig_idx) # Store original index
- index_item.setFlags(index_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
- self.tokens_table.setItem(row, 0, index_item)
- # Token
- token_item = QTableWidgetItem(str(self.tokens[orig_idx]))
- self.tokens_table.setItem(row, 1, token_item)
- # Token Type
- token_type = self.token_types[orig_idx] if orig_idx < len(self.token_types) else 0
- try:
- enum_val = TokenType(token_type)
- display_text = f"{enum_val.name} ({token_type})"
- except (ValueError, KeyError):
- display_text = f"Unknown ({token_type})"
- type_item = QTableWidgetItem(display_text)
- type_item.setData(Qt.ItemDataRole.UserRole, token_type)
- # Make type cell editable with a double-click handler
- type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
- self.tokens_table.setItem(row, 2, type_item)
- # Score
- score = self.scores[orig_idx] if orig_idx < len(self.scores) else 0.0
- score_item = QTableWidgetItem(str(score))
- self.tokens_table.setItem(row, 3, score_item)
- # Connect double-click handler for token type cells
- self.tokens_table.cellDoubleClicked.connect(self.handle_cell_double_click)
- def handle_cell_double_click(self, row, column):
- """Handle double-click on a cell, specifically for token type editing."""
- if column == 2: # Token Type column
- orig_item = self.tokens_table.item(row, 0)
- if orig_item:
- orig_idx = orig_item.data(Qt.ItemDataRole.UserRole)
- self.edit_token_type(row, orig_idx)
- def edit_token_type(self, row, orig_idx):
- """Edit a token type using a dialog with a dropdown of all enum options."""
- current_value = self.token_types[orig_idx] if orig_idx < len(self.token_types) else 0
- # Create a dialog with enum options
- dialog = QDialog(self)
- dialog.setWindowTitle("Select Token Type")
- layout = QVBoxLayout(dialog)
- combo = QComboBox()
- for enum_val in TokenType:
- combo.addItem(f"{enum_val.name} ({enum_val.value})", enum_val.value)
- # Set current value
- try:
- if isinstance(current_value, int):
- enum_val = TokenType(current_value)
- combo.setCurrentText(f"{enum_val.name} ({current_value})")
- except (ValueError, KeyError):
- pass
- layout.addWidget(combo)
- buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
- buttons.accepted.connect(dialog.accept)
- buttons.rejected.connect(dialog.reject)
- layout.addWidget(buttons)
- if dialog.exec() == QDialog.DialogCode.Accepted:
- # Get the selected value
- new_value = combo.currentData()
- enum_val = TokenType(new_value)
- display_text = f"{enum_val.name} ({new_value})"
- # Update the display
- type_item = self.tokens_table.item(row, 2)
- if type_item:
- type_item.setText(display_text)
- type_item.setData(Qt.ItemDataRole.UserRole, new_value)
- # Update the actual value
- self.token_types[orig_idx] = new_value
- def add_token(self):
- """Add a new token to the end of the list."""
- # Add to the end of the arrays
- self.tokens.append("")
- self.token_types.append(0) # Default to normal token
- self.scores.append(0.0)
- orig_idx = len(self.tokens) - 1
- # Add to filtered indices if it matches the current filter
- filter_text = self.filter_edit.text().lower()
- if not filter_text or filter_text in "":
- self.filtered_indices.append(orig_idx)
- # Update pagination
- self.total_pages = max(1, (len(self.filtered_indices) + self.page_size - 1) // self.page_size)
- # Go to the last page to show the new item
- self.current_page = self.total_pages - 1
- self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
- # Reload the page
- self.load_page()
- def remove_selected(self):
- """Remove selected tokens from all arrays."""
- selected_rows = []
- for item in self.tokens_table.selectedItems():
- row = item.row()
- if row not in selected_rows:
- selected_rows.append(row)
- if not selected_rows:
- return
- # Get original indices in descending order to avoid index shifting
- orig_indices = []
- for row in selected_rows:
- orig_item = self.tokens_table.item(row, 0)
- if orig_item:
- orig_indices.append(orig_item.data(Qt.ItemDataRole.UserRole))
- orig_indices.sort(reverse=True)
- # Remove from all arrays
- for idx in orig_indices:
- if idx < len(self.tokens):
- del self.tokens[idx]
- if idx < len(self.token_types):
- del self.token_types[idx]
- if idx < len(self.scores):
- del self.scores[idx]
- # Rebuild filtered_indices
- self.filtered_indices = []
- filter_text = self.filter_edit.text().lower()
- for i, token in enumerate(self.tokens):
- if not filter_text or filter_text in str(token).lower():
- self.filtered_indices.append(i)
- # Update pagination
- self.total_pages = max(1, (len(self.filtered_indices) + self.page_size - 1) // self.page_size)
- self.current_page = min(self.current_page, self.total_pages - 1)
- self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
- # Reload the page
- self.load_page()
- def get_data(self):
- """Return the edited tokenizer data."""
- return self.tokens, self.token_types, self.scores
- class ArrayEditorDialog(QDialog):
- def __init__(self, array_values, element_type, key=None, parent=None):
- super().__init__(parent)
- self.setWindowTitle("Edit Array Values")
- self.resize(700, 500)
- self.array_values = array_values
- self.element_type = element_type
- self.key = key
- # Get enum type for this array if applicable
- self.enum_type = None
- if key in KEY_TO_ENUM_TYPE and element_type == GGUFValueType.INT32:
- self.enum_type = KEY_TO_ENUM_TYPE[key]
- layout = QVBoxLayout(self)
- # Add enum type information if applicable
- if self.enum_type is not None:
- enum_info_layout = QHBoxLayout()
- enum_label = QLabel(f"Editing {self.enum_type.__name__} values:")
- enum_info_layout.addWidget(enum_label)
- # Add a legend for the enum values
- enum_values = ", ".join([f"{e.name}={e.value}" for e in self.enum_type])
- enum_values_label = QLabel(f"Available values: {enum_values}")
- enum_values_label.setWordWrap(True)
- enum_info_layout.addWidget(enum_values_label, 1)
- layout.addLayout(enum_info_layout)
- # Add search/filter controls
- filter_layout = QHBoxLayout()
- filter_layout.addWidget(QLabel("Filter:"))
- self.filter_edit = QLineEdit()
- self.filter_edit.setPlaceholderText("Type to filter values...")
- self.filter_edit.textChanged.connect(self.apply_filter)
- filter_layout.addWidget(self.filter_edit)
- # Add page controls for large arrays
- self.page_size = 100 # Show 100 items per page
- self.current_page = 0
- self.total_pages = max(1, (len(array_values) + self.page_size - 1) // self.page_size)
- self.page_label = QLabel(f"Page 1 of {self.total_pages}")
- filter_layout.addWidget(self.page_label)
- prev_page = QPushButton("Previous")
- prev_page.clicked.connect(self.previous_page)
- filter_layout.addWidget(prev_page)
- next_page = QPushButton("Next")
- next_page.clicked.connect(self.next_page)
- filter_layout.addWidget(next_page)
- layout.addLayout(filter_layout)
- # Array items table
- self.items_table = QTableWidget()
- # Set up columns based on whether we have an enum type
- if self.enum_type is not None:
- self.items_table.setColumnCount(3)
- self.items_table.setHorizontalHeaderLabels(["Index", "Value", "Actions"])
- self.items_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
- self.items_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
- self.items_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
- else:
- self.items_table.setColumnCount(2)
- self.items_table.setHorizontalHeaderLabels(["Index", "Value"])
- self.items_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
- self.items_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
- layout.addWidget(self.items_table)
- # Controls
- controls_layout = QHBoxLayout()
- add_button = QPushButton("Add Item")
- add_button.clicked.connect(self.add_item)
- controls_layout.addWidget(add_button)
- remove_button = QPushButton("Remove Selected")
- remove_button.clicked.connect(self.remove_selected)
- controls_layout.addWidget(remove_button)
- # Add bulk edit button for enum arrays
- if self.enum_type is not None:
- bulk_edit_button = QPushButton("Bulk Edit Selected")
- bulk_edit_button.clicked.connect(self.bulk_edit_selected)
- controls_layout.addWidget(bulk_edit_button)
- controls_layout.addStretch()
- layout.addLayout(controls_layout)
- # Buttons
- buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
- buttons.accepted.connect(self.accept)
- buttons.rejected.connect(self.reject)
- layout.addWidget(buttons)
- # Initialize the filtered values
- self.filtered_indices = list(range(len(self.array_values)))
- # Load array values for the first page
- self.load_page()
- def apply_filter(self):
- """Filter the array values based on the search text."""
- filter_text = self.filter_edit.text().lower()
- if not filter_text:
- # No filter, show all values
- self.filtered_indices = list(range(len(self.array_values)))
- else:
- # Apply filter
- self.filtered_indices = []
- for i, value in enumerate(self.array_values):
- # For enum values, search in both name and value
- if self.enum_type is not None and isinstance(value, int):
- try:
- enum_val = self.enum_type(value)
- display_text = f"{enum_val.name} ({value})".lower()
- if filter_text in display_text:
- self.filtered_indices.append(i)
- except (ValueError, KeyError):
- # If not a valid enum value, just check the raw value
- if filter_text in str(value).lower():
- self.filtered_indices.append(i)
- else:
- # For non-enum values, just check the string representation
- if filter_text in str(value).lower():
- self.filtered_indices.append(i)
- # Reset to first page and reload
- self.total_pages = max(1, (len(self.filtered_indices) + self.page_size - 1) // self.page_size)
- self.current_page = 0
- self.page_label.setText(f"Page 1 of {self.total_pages}")
- self.load_page()
- def previous_page(self):
- """Go to the previous page of results."""
- if self.current_page > 0:
- self.current_page -= 1
- self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
- self.load_page()
- def next_page(self):
- """Go to the next page of results."""
- if self.current_page < self.total_pages - 1:
- self.current_page += 1
- self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
- self.load_page()
- def load_page(self):
- """Load the current page of array values."""
- self.items_table.setRowCount(0) # Clear the table
- # Calculate start and end indices for the current page
- start_idx = self.current_page * self.page_size
- end_idx = min(start_idx + self.page_size, len(self.filtered_indices))
- # Pre-allocate rows for better performance
- self.items_table.setRowCount(end_idx - start_idx)
- for row, i in enumerate(range(start_idx, end_idx)):
- orig_idx = self.filtered_indices[i]
- value = self.array_values[orig_idx]
- # Index
- index_item = QTableWidgetItem(str(orig_idx))
- index_item.setData(Qt.ItemDataRole.UserRole, orig_idx) # Store original index
- index_item.setFlags(index_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
- self.items_table.setItem(row, 0, index_item)
- # Value
- if self.enum_type is not None:
- # Display enum value and name
- try:
- if isinstance(value, (int, numpy.signedinteger)):
- enum_val = self.enum_type(value)
- display_text = f"{enum_val.name} ({value})"
- else:
- display_text = str(value)
- except (ValueError, KeyError):
- display_text = f"Unknown ({value})"
- # Store the enum value in the item
- value_item = QTableWidgetItem(display_text)
- value_item.setData(Qt.ItemDataRole.UserRole, value)
- value_item.setFlags(value_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
- self.items_table.setItem(row, 1, value_item)
- # Add an edit button in a separate column
- edit_button = QPushButton("Edit")
- edit_button.setProperty("row", row)
- edit_button.clicked.connect(self.edit_array_enum_value)
- # Create a widget to hold the button
- button_widget = QWidget()
- button_layout = QHBoxLayout(button_widget)
- button_layout.setContentsMargins(2, 2, 2, 2)
- button_layout.addWidget(edit_button)
- button_layout.addStretch()
- self.items_table.setCellWidget(row, 2, button_widget)
- else:
- value_item = QTableWidgetItem(str(value))
- self.items_table.setItem(row, 1, value_item)
- def edit_array_enum_value(self):
- """Handle editing an enum value in the array editor."""
- button = self.sender()
- row = button.property("row")
- # Get the original index from the table item
- orig_item = self.items_table.item(row, 0)
- new_item = self.items_table.item(row, 1)
- if orig_item and new_item and self.enum_type and self.edit_enum_value(row, self.enum_type):
- orig_idx = orig_item.data(Qt.ItemDataRole.UserRole)
- new_value = new_item.data(Qt.ItemDataRole.UserRole)
- # Update the stored value in the array
- if isinstance(new_value, (int, float, str, bool)):
- self.array_values[orig_idx] = new_value
- def bulk_edit_selected(self):
- """Edit multiple enum values at once."""
- if not self.enum_type:
- return
- selected_rows = set()
- for item in self.items_table.selectedItems():
- selected_rows.add(item.row())
- if not selected_rows:
- QMessageBox.information(self, "No Selection", "Please select at least one row to edit.")
- return
- # Create a dialog with enum options
- dialog = QDialog(self)
- dialog.setWindowTitle(f"Bulk Edit {self.enum_type.__name__} Values")
- layout = QVBoxLayout(dialog)
- layout.addWidget(QLabel(f"Set {len(selected_rows)} selected items to:"))
- combo = QComboBox()
- for enum_val in self.enum_type:
- combo.addItem(f"{enum_val.name} ({enum_val.value})", enum_val.value)
- layout.addWidget(combo)
- buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
- buttons.accepted.connect(dialog.accept)
- buttons.rejected.connect(dialog.reject)
- layout.addWidget(buttons)
- if dialog.exec() == QDialog.DialogCode.Accepted:
- # Get the selected value
- new_value = combo.currentData()
- enum_val = self.enum_type(new_value)
- display_text = f"{enum_val.name} ({new_value})"
- # Update all selected rows
- for row in selected_rows:
- orig_item = self.items_table.item(row, 0)
- new_item = self.items_table.item(row, 1)
- if orig_item and new_item:
- orig_idx = orig_item.data(Qt.ItemDataRole.UserRole)
- self.array_values[orig_idx] = new_value
- # Update the display
- new_item.setText(display_text)
- new_item.setData(Qt.ItemDataRole.UserRole, new_value)
- def add_item(self):
- # Add to the end of the array
- orig_idx = len(self.array_values)
- # Add default value based on type
- if self.enum_type is not None:
- # Default to first enum value
- default_value = list(self.enum_type)[0].value
- self.array_values.append(default_value)
- else:
- if self.element_type == GGUFValueType.STRING:
- self.array_values.append("")
- else:
- self.array_values.append(0)
- # Add to filtered indices if it matches the current filter
- self.filtered_indices.append(orig_idx)
- # Update pagination
- self.total_pages = max(1, (len(self.filtered_indices) + self.page_size - 1) // self.page_size)
- # Go to the last page to show the new item
- self.current_page = self.total_pages - 1
- self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
- # Reload the page
- self.load_page()
- def remove_selected(self):
- selected_rows = []
- for item in self.items_table.selectedItems():
- row = item.row()
- if row not in selected_rows:
- selected_rows.append(row)
- if not selected_rows:
- return
- # Get original indices in descending order to avoid index shifting
- orig_indices = list()
- for row in selected_rows:
- orig_item = self.items_table.item(row, 0)
- if orig_item:
- orig_indices.append(orig_item.data(Qt.ItemDataRole.UserRole))
- orig_indices.sort(reverse=True)
- # Remove from array_values
- for idx in orig_indices:
- del self.array_values[idx]
- # Rebuild filtered_indices
- self.filtered_indices = []
- filter_text = self.filter_edit.text().lower()
- for i, value in enumerate(self.array_values):
- if not filter_text:
- self.filtered_indices.append(i)
- else:
- # Apply filter
- if self.enum_type is not None and isinstance(value, int):
- try:
- enum_val = self.enum_type(value)
- display_text = f"{enum_val.name} ({value})".lower()
- if filter_text in display_text:
- self.filtered_indices.append(i)
- except (ValueError, KeyError):
- if filter_text in str(value).lower():
- self.filtered_indices.append(i)
- else:
- if filter_text in str(value).lower():
- self.filtered_indices.append(i)
- # Update pagination
- self.total_pages = max(1, (len(self.filtered_indices) + self.page_size - 1) // self.page_size)
- self.current_page = min(self.current_page, self.total_pages - 1)
- self.page_label.setText(f"Page {self.current_page + 1} of {self.total_pages}")
- # Reload the page
- self.load_page()
- def edit_enum_value(self, row: int, enum_type: Type[enum.Enum]):
- """Edit an enum value using a dialog with a dropdown of all enum options."""
- # Get the original index from the table item
- orig_item = self.items_table.item(row, 0)
- if orig_item:
- orig_idx = orig_item.data(Qt.ItemDataRole.UserRole)
- else:
- return
- current_value = self.array_values[orig_idx]
- # Create a dialog with enum options
- dialog = QDialog(self)
- dialog.setWindowTitle(f"Select {enum_type.__name__} Value")
- layout = QVBoxLayout(dialog)
- # Add description
- description = QLabel(f"Select a {enum_type.__name__} value:")
- layout.addWidget(description)
- # Use a combo box for quick selection
- combo = QComboBox()
- for enum_val in enum_type:
- combo.addItem(f"{enum_val.name} ({enum_val.value})", enum_val.value)
- # Set current value
- try:
- if isinstance(current_value, int):
- enum_val = enum_type(current_value)
- combo.setCurrentText(f"{enum_val.name} ({current_value})")
- except (ValueError, KeyError):
- pass
- layout.addWidget(combo)
- buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
- buttons.accepted.connect(dialog.accept)
- buttons.rejected.connect(dialog.reject)
- layout.addWidget(buttons)
- if dialog.exec() == QDialog.DialogCode.Accepted:
- # Update the value display and stored data
- new_value = combo.currentData()
- enum_val = enum_type(new_value)
- display_text = f"{enum_val.name} ({new_value})"
- new_item = self.items_table.item(row, 1)
- if new_item:
- new_item.setText(display_text)
- new_item.setData(Qt.ItemDataRole.UserRole, new_value)
- # Update the actual array value
- self.array_values[orig_idx] = new_value
- return True
- return False
- def get_array_values(self):
- # The array_values list is kept up-to-date as edits are made
- return self.array_values
- class AddMetadataDialog(QDialog):
- def __init__(self, parent=None):
- super().__init__(parent)
- self.setWindowTitle("Add Metadata")
- self.resize(400, 200)
- layout = QVBoxLayout(self)
- form_layout = QFormLayout()
- self.key_edit = QLineEdit()
- form_layout.addRow("Key:", self.key_edit)
- self.type_combo = QComboBox()
- for value_type in GGUFValueType:
- if value_type != GGUFValueType.ARRAY: # Skip array type for simplicity
- self.type_combo.addItem(value_type.name, value_type)
- form_layout.addRow("Type:", self.type_combo)
- self.value_edit = QTextEdit()
- form_layout.addRow("Value:", self.value_edit)
- layout.addLayout(form_layout)
- buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
- buttons.accepted.connect(self.accept)
- buttons.rejected.connect(self.reject)
- layout.addWidget(buttons)
- def get_data(self) -> Tuple[str, GGUFValueType, Any]:
- key = self.key_edit.text()
- value_type = self.type_combo.currentData()
- value_text = self.value_edit.toPlainText()
- # Convert value based on type
- if value_type == GGUFValueType.UINT8:
- value = np.uint8(int(value_text))
- elif value_type == GGUFValueType.INT8:
- value = np.int8(int(value_text))
- elif value_type == GGUFValueType.UINT16:
- value = np.uint16(int(value_text))
- elif value_type == GGUFValueType.INT16:
- value = np.int16(int(value_text))
- elif value_type == GGUFValueType.UINT32:
- value = np.uint32(int(value_text))
- elif value_type == GGUFValueType.INT32:
- value = np.int32(int(value_text))
- elif value_type == GGUFValueType.FLOAT32:
- value = np.float32(float(value_text))
- elif value_type == GGUFValueType.BOOL:
- value = value_text.lower() in ('true', 'yes', '1')
- elif value_type == GGUFValueType.STRING:
- value = value_text
- else:
- value = value_text
- return key, value_type, value
- class GGUFEditorWindow(QMainWindow):
- def __init__(self):
- super().__init__()
- self.setWindowTitle("GGUF Editor")
- self.resize(1000, 800)
- self.current_file = None
- self.reader = None
- self.modified = False
- self.metadata_changes = {} # Store changes to apply when saving
- self.metadata_to_remove = set() # Store keys to remove when saving
- self.on_metadata_changed_is_connected = False
- self.setup_ui()
- def setup_ui(self):
- central_widget = QWidget()
- self.setCentralWidget(central_widget)
- main_layout = QVBoxLayout(central_widget)
- # File controls
- file_layout = QHBoxLayout()
- self.file_path_edit = QLineEdit()
- self.file_path_edit.setReadOnly(True)
- file_layout.addWidget(self.file_path_edit)
- open_button = QPushButton("Open GGUF")
- open_button.clicked.connect(self.open_file)
- file_layout.addWidget(open_button)
- save_button = QPushButton("Save As...")
- save_button.clicked.connect(self.save_file)
- file_layout.addWidget(save_button)
- main_layout.addLayout(file_layout)
- # Tabs for different views
- self.tabs = QTabWidget()
- # Metadata tab
- self.metadata_tab = QWidget()
- metadata_layout = QVBoxLayout(self.metadata_tab)
- # Metadata table
- self.metadata_table = QTableWidget()
- self.metadata_table.setColumnCount(4)
- self.metadata_table.setHorizontalHeaderLabels(["Key", "Type", "Value", "Actions"])
- self.metadata_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
- self.metadata_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
- self.metadata_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
- self.metadata_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
- metadata_layout.addWidget(self.metadata_table)
- # Metadata controls
- metadata_controls = QHBoxLayout()
- add_metadata_button = QPushButton("Add Metadata")
- add_metadata_button.clicked.connect(self.add_metadata)
- metadata_controls.addWidget(add_metadata_button)
- metadata_controls.addStretch()
- metadata_layout.addLayout(metadata_controls)
- # Tensors tab
- self.tensors_tab = QWidget()
- tensors_layout = QVBoxLayout(self.tensors_tab)
- self.tensors_table = QTableWidget()
- self.tensors_table.setColumnCount(5)
- self.tensors_table.setHorizontalHeaderLabels(["Name", "Type", "Shape", "Elements", "Size (bytes)"])
- self.tensors_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
- self.tensors_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
- self.tensors_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
- self.tensors_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
- self.tensors_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents)
- tensors_layout.addWidget(self.tensors_table)
- # Add tabs to tab widget
- self.tabs.addTab(self.metadata_tab, "Metadata")
- self.tabs.addTab(self.tensors_tab, "Tensors")
- main_layout.addWidget(self.tabs)
- # Status bar
- self.statusBar().showMessage("Ready")
- def load_file(self, file_path):
- """Load a GGUF file by path"""
- try:
- self.statusBar().showMessage(f"Loading {file_path}...")
- QApplication.processEvents()
- self.reader = GGUFReader(file_path, 'r')
- self.current_file = file_path
- self.file_path_edit.setText(file_path)
- self.load_metadata()
- self.load_tensors()
- self.metadata_changes = {}
- self.metadata_to_remove = set()
- self.modified = False
- self.statusBar().showMessage(f"Loaded {file_path}")
- return True
- except Exception as e:
- QMessageBox.critical(self, "Error", f"Failed to open file: {str(e)}")
- self.statusBar().showMessage("Error loading file")
- return False
- def open_file(self):
- file_path, _ = QFileDialog.getOpenFileName(
- self, "Open GGUF File", "", "GGUF Files (*.gguf);;All Files (*)"
- )
- if not file_path:
- return
- self.load_file(file_path)
- def load_metadata(self):
- self.metadata_table.setRowCount(0)
- if not self.reader:
- return
- # Disconnect to prevent triggering during loading
- if self.on_metadata_changed_is_connected:
- with warnings.catch_warnings():
- warnings.filterwarnings('ignore')
- self.metadata_table.itemChanged.disconnect(self.on_metadata_changed)
- self.on_metadata_changed_is_connected = False
- for i, (key, field) in enumerate(self.reader.fields.items()):
- self.metadata_table.insertRow(i)
- # Key
- key_item = QTableWidgetItem(key)
- key_item.setFlags(key_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
- self.metadata_table.setItem(i, 0, key_item)
- # Type
- if not field.types:
- type_str = "N/A"
- elif field.types[0] == GGUFValueType.ARRAY:
- nest_count = len(field.types) - 1
- element_type = field.types[-1].name
- # Check if this is an enum array
- enum_type = self.get_enum_for_key(key)
- if enum_type is not None and field.types[-1] == GGUFValueType.INT32:
- element_type = enum_type.__name__
- type_str = '[' * nest_count + element_type + ']' * nest_count
- else:
- type_str = str(field.types[0].name)
- # Check if this is an enum field
- enum_type = self.get_enum_for_key(key)
- if enum_type is not None and field.types[0] == GGUFValueType.INT32:
- type_str = enum_type.__name__
- type_item = QTableWidgetItem(type_str)
- type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
- self.metadata_table.setItem(i, 1, type_item)
- # Value
- value_str = self.format_field_value(field)
- value_item = QTableWidgetItem(value_str)
- # Make only simple values editable
- if len(field.types) == 1 and field.types[0] != GGUFValueType.ARRAY:
- value_item.setFlags(value_item.flags() | Qt.ItemFlag.ItemIsEditable)
- else:
- value_item.setFlags(value_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
- self.metadata_table.setItem(i, 2, value_item)
- # Actions
- actions_widget = QWidget()
- actions_layout = QHBoxLayout(actions_widget)
- actions_layout.setContentsMargins(2, 2, 2, 2)
- # Add Edit button for arrays and enum fields
- if field.types and field.types[0] == GGUFValueType.ARRAY:
- edit_button = QPushButton("Edit")
- edit_button.setProperty("row", i)
- edit_button.setProperty("key", key)
- edit_button.clicked.connect(self.edit_array_metadata)
- actions_layout.addWidget(edit_button)
- # Add special label for tokenizer linked fields
- if key in TOKENIZER_LINKED_KEYS:
- edit_button.setText("Edit Tokenizer")
- edit_button.setToolTip("Edit all tokenizer data together")
- elif len(field.types) == 1 and self.get_enum_for_key(key) is not None:
- edit_button = QPushButton("Edit")
- edit_button.setProperty("row", i)
- edit_button.setProperty("key", key)
- edit_button.clicked.connect(self.edit_metadata_enum)
- actions_layout.addWidget(edit_button)
- remove_button = QPushButton("Remove")
- remove_button.setProperty("row", i)
- remove_button.setProperty("key", key)
- remove_button.clicked.connect(self.remove_metadata)
- actions_layout.addWidget(remove_button)
- self.metadata_table.setCellWidget(i, 3, actions_widget)
- # Reconnect after loading
- self.metadata_table.itemChanged.connect(self.on_metadata_changed)
- self.on_metadata_changed_is_connected = True
- def extract_array_values(self, field: ReaderField) -> list:
- """Extract all values from an array field."""
- if not field.types or field.types[0] != GGUFValueType.ARRAY:
- return []
- curr_type = field.types[1]
- array_values = []
- total_elements = len(field.data)
- if curr_type == GGUFValueType.STRING:
- for element_pos in range(total_elements):
- value_string = str(bytes(field.parts[-1 - (total_elements - element_pos - 1) * 2]), encoding='utf-8')
- array_values.append(value_string)
- elif self.reader and curr_type in self.reader.gguf_scalar_to_np:
- for element_pos in range(total_elements):
- array_values.append(field.parts[-1 - (total_elements - element_pos - 1)][0])
- return array_values
- def get_enum_for_key(self, key: str) -> Optional[Type[enum.Enum]]:
- """Get the enum type for a given key if it exists."""
- return KEY_TO_ENUM_TYPE.get(key)
- def format_enum_value(self, value: Any, enum_type: Type[enum.Enum]) -> str:
- """Format a value as an enum if possible."""
- try:
- if isinstance(value, (int, str)):
- enum_value = enum_type(value)
- return f"{enum_value.name} ({value})"
- except (ValueError, KeyError):
- pass
- return str(value)
- def format_field_value(self, field: ReaderField) -> str:
- if not field.types:
- return "N/A"
- if len(field.types) == 1:
- curr_type = field.types[0]
- if curr_type == GGUFValueType.STRING:
- return str(bytes(field.parts[-1]), encoding='utf-8')
- elif self.reader and curr_type in self.reader.gguf_scalar_to_np:
- value = field.parts[-1][0]
- # Check if this field has an enum type
- enum_type = self.get_enum_for_key(field.name)
- if enum_type is not None:
- return self.format_enum_value(value, enum_type)
- return str(value)
- if field.types[0] == GGUFValueType.ARRAY:
- array_values = self.extract_array_values(field)
- render_element = min(5, len(array_values))
- # Get enum type for this array if applicable
- enum_type = self.get_enum_for_key(field.name)
- if enum_type is not None:
- array_elements = []
- for i in range(render_element):
- array_elements.append(self.format_enum_value(array_values[i], enum_type))
- else:
- array_elements = [str(array_values[i]) for i in range(render_element)]
- return f"[ {', '.join(array_elements).strip()}{', ...' if len(array_values) > len(array_elements) else ''} ]"
- return "Complex value"
- def load_tensors(self):
- self.tensors_table.setRowCount(0)
- if not self.reader:
- return
- for i, tensor in enumerate(self.reader.tensors):
- self.tensors_table.insertRow(i)
- # Name
- name_item = QTableWidgetItem(tensor.name)
- name_item.setFlags(name_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
- self.tensors_table.setItem(i, 0, name_item)
- # Type
- type_item = QTableWidgetItem(tensor.tensor_type.name)
- type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
- self.tensors_table.setItem(i, 1, type_item)
- # Shape
- shape_str = " × ".join(str(d) for d in tensor.shape)
- shape_item = QTableWidgetItem(shape_str)
- shape_item.setFlags(shape_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
- self.tensors_table.setItem(i, 2, shape_item)
- # Elements
- elements_item = QTableWidgetItem(str(tensor.n_elements))
- elements_item.setFlags(elements_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
- self.tensors_table.setItem(i, 3, elements_item)
- # Size
- size_item = QTableWidgetItem(f"{tensor.n_bytes:,}")
- size_item.setFlags(size_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
- self.tensors_table.setItem(i, 4, size_item)
- def on_metadata_changed(self, item):
- if item.column() != 2: # Only handle value column changes
- return
- row = item.row()
- orig_item = self.metadata_table.item(row, 0)
- key = None
- if orig_item:
- key = orig_item.text()
- new_value = item.text()
- field = None
- if self.reader and key:
- field = self.reader.get_field(key)
- if not field or not field.types or not key:
- return
- value_type = field.types[0]
- # Check if this is an enum field
- enum_type = self.get_enum_for_key(key)
- if enum_type is not None and value_type == GGUFValueType.INT32:
- # Try to parse the enum value from the text
- try:
- # Check if it's a name
- try:
- enum_val = enum_type[new_value]
- converted_value = enum_val.value
- except (KeyError, AttributeError):
- # Check if it's a number or "NAME (value)" format
- if '(' in new_value and ')' in new_value:
- # Extract the value from "NAME (value)" format
- value_part = new_value.split('(')[1].split(')')[0].strip()
- converted_value = int(value_part)
- else:
- # Try to convert directly to int
- converted_value = int(new_value)
- # Validate that it's a valid enum value
- enum_type(converted_value)
- # Store the change
- self.metadata_changes[key] = (value_type, converted_value)
- self.modified = True
- # Update display with formatted enum value
- formatted_value = self.format_enum_value(converted_value, enum_type)
- item.setText(formatted_value)
- self.statusBar().showMessage(f"Changed {key} to {formatted_value}")
- return
- except (ValueError, KeyError) as e:
- QMessageBox.warning(
- self,
- f"Invalid Enum Value ({e})",
- f"'{new_value}' is not a valid {enum_type.__name__} value.\n"
- f"Valid values are: {', '.join(v.name for v in enum_type)}")
- # Revert to original value
- original_value = self.format_field_value(field)
- item.setText(original_value)
- return
- try:
- # Convert the string value to the appropriate type
- if value_type == GGUFValueType.UINT8:
- converted_value = np.uint8(int(new_value))
- elif value_type == GGUFValueType.INT8:
- converted_value = np.int8(int(new_value))
- elif value_type == GGUFValueType.UINT16:
- converted_value = np.uint16(int(new_value))
- elif value_type == GGUFValueType.INT16:
- converted_value = np.int16(int(new_value))
- elif value_type == GGUFValueType.UINT32:
- converted_value = np.uint32(int(new_value))
- elif value_type == GGUFValueType.INT32:
- converted_value = np.int32(int(new_value))
- elif value_type == GGUFValueType.FLOAT32:
- converted_value = np.float32(float(new_value))
- elif value_type == GGUFValueType.BOOL:
- converted_value = new_value.lower() in ('true', 'yes', '1')
- elif value_type == GGUFValueType.STRING:
- converted_value = new_value
- else:
- # Unsupported type for editing
- return
- # Store the change
- self.metadata_changes[key] = (value_type, converted_value)
- self.modified = True
- self.statusBar().showMessage(f"Changed {key} to {new_value}")
- except ValueError:
- QMessageBox.warning(self, "Invalid Value", f"The value '{new_value}' is not valid for type {value_type.name}")
- # Revert to original value
- original_value = self.format_field_value(field)
- item.setText(original_value)
- def remove_metadata(self):
- button = self.sender()
- key = button.property("key")
- row = button.property("row")
- reply = QMessageBox.question(
- self, "Confirm Removal",
- f"Are you sure you want to remove the metadata key '{key}'?",
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No
- )
- if reply == QMessageBox.StandardButton.Yes:
- self.metadata_table.removeRow(row)
- self.metadata_to_remove.add(key)
- # If we previously had changes for this key, remove them
- if key in self.metadata_changes:
- del self.metadata_changes[key]
- self.modified = True
- self.statusBar().showMessage(f"Marked {key} for removal")
- def edit_metadata_enum(self):
- """Edit an enum metadata field."""
- button = self.sender()
- key = button.property("key")
- row = button.property("row")
- field = None
- if self.reader:
- field = self.reader.get_field(key)
- if not field or not field.types:
- return
- enum_type = self.get_enum_for_key(key)
- if enum_type is None:
- return
- # Get current value
- current_value = field.contents()
- # Create a dialog with enum options
- dialog = QDialog(self)
- dialog.setWindowTitle(f"Select {enum_type.__name__} Value")
- layout = QVBoxLayout(dialog)
- combo = QComboBox()
- for enum_val in enum_type:
- combo.addItem(f"{enum_val.name} ({enum_val.value})", enum_val.value)
- # Set current value
- try:
- if isinstance(current_value, (int, str)):
- enum_val = enum_type(current_value)
- combo.setCurrentText(f"{enum_val.name} ({current_value})")
- except (ValueError, KeyError):
- pass
- layout.addWidget(combo)
- buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
- buttons.accepted.connect(dialog.accept)
- buttons.rejected.connect(dialog.reject)
- layout.addWidget(buttons)
- if dialog.exec() == QDialog.DialogCode.Accepted:
- # Get the selected value
- new_value = combo.currentData()
- enum_val = enum_type(new_value)
- # Store the change
- self.metadata_changes[key] = (field.types[0], new_value)
- self.modified = True
- # Update display
- display_text = f"{enum_val.name} ({new_value})"
- target_item = self.metadata_table.item(row, 2)
- if target_item:
- target_item.setText(display_text)
- self.statusBar().showMessage(f"Changed {key} to {display_text}")
- def edit_array_metadata(self):
- button = self.sender()
- key = button.property("key")
- row = button.property("row")
- # Check if this is one of the linked tokenizer keys
- if key in TOKENIZER_LINKED_KEYS:
- self.edit_tokenizer_metadata(key)
- return
- field = None
- if self.reader:
- field = self.reader.get_field(key)
- if not field or not field.types or field.types[0] != GGUFValueType.ARRAY:
- return
- # Get array element type
- element_type = field.types[1]
- # Extract array values
- array_values = self.extract_array_values(field)
- # Open array editor dialog
- dialog = ArrayEditorDialog(array_values, element_type, key, self)
- if dialog.exec() == QDialog.DialogCode.Accepted:
- new_values = dialog.get_array_values()
- # Store the change
- self.metadata_changes[key] = (GGUFValueType.ARRAY, (element_type, new_values))
- self.modified = True
- # Update display
- enum_type = self.get_enum_for_key(key)
- if enum_type is not None and element_type == GGUFValueType.INT32:
- value_str = f"[ {', '.join(self.format_enum_value(v, enum_type) for v in new_values[:5])}{', ...' if len(new_values) > 5 else ''} ]"
- else:
- value_str = f"[ {', '.join(str(v) for v in new_values[:5])}{', ...' if len(new_values) > 5 else ''} ]"
- target_item = self.metadata_table.item(row, 2)
- if target_item:
- target_item.setText(value_str)
- self.statusBar().showMessage(f"Updated array values for {key}")
- def edit_tokenizer_metadata(self, trigger_key):
- """Edit the linked tokenizer metadata arrays together."""
- if not self.reader:
- return
- # Get all three fields
- tokens_field = self.reader.get_field(gguf.Keys.Tokenizer.LIST)
- token_types_field = self.reader.get_field(gguf.Keys.Tokenizer.TOKEN_TYPE)
- scores_field = self.reader.get_field(gguf.Keys.Tokenizer.SCORES)
- # Extract values from each field
- tokens = self.extract_array_values(tokens_field) if tokens_field else []
- token_types = self.extract_array_values(token_types_field) if token_types_field else []
- scores = self.extract_array_values(scores_field) if scores_field else []
- # Apply any pending changes
- if gguf.Keys.Tokenizer.LIST in self.metadata_changes:
- _, (_, tokens) = self.metadata_changes[gguf.Keys.Tokenizer.LIST]
- if gguf.Keys.Tokenizer.TOKEN_TYPE in self.metadata_changes:
- _, (_, token_types) = self.metadata_changes[gguf.Keys.Tokenizer.TOKEN_TYPE]
- if gguf.Keys.Tokenizer.SCORES in self.metadata_changes:
- _, (_, scores) = self.metadata_changes[gguf.Keys.Tokenizer.SCORES]
- # Open the tokenizer editor dialog
- dialog = TokenizerEditorDialog(tokens, token_types, scores, self)
- if dialog.exec() == QDialog.DialogCode.Accepted:
- new_tokens, new_token_types, new_scores = dialog.get_data()
- # Store changes for all three arrays
- if tokens_field:
- self.metadata_changes[gguf.Keys.Tokenizer.LIST] = (
- GGUFValueType.ARRAY,
- (tokens_field.types[1], new_tokens)
- )
- if token_types_field:
- self.metadata_changes[gguf.Keys.Tokenizer.TOKEN_TYPE] = (
- GGUFValueType.ARRAY,
- (token_types_field.types[1], new_token_types)
- )
- if scores_field:
- self.metadata_changes[gguf.Keys.Tokenizer.SCORES] = (
- GGUFValueType.ARRAY,
- (scores_field.types[1], new_scores)
- )
- self.modified = True
- # Update display for all three fields
- self.update_tokenizer_display(gguf.Keys.Tokenizer.LIST, new_tokens)
- self.update_tokenizer_display(gguf.Keys.Tokenizer.TOKEN_TYPE, new_token_types)
- self.update_tokenizer_display(gguf.Keys.Tokenizer.SCORES, new_scores)
- self.statusBar().showMessage("Updated tokenizer data")
- def update_tokenizer_display(self, key, values):
- """Update the display of a tokenizer field in the metadata table."""
- for row in range(self.metadata_table.rowCount()):
- key_item = self.metadata_table.item(row, 0)
- if key_item and key_item.text() == key:
- value_str = f"[ {', '.join(str(v) for v in values[:5])}{', ...' if len(values) > 5 else ''} ]"
- value_item = self.metadata_table.item(row, 2)
- if value_item:
- value_item.setText(value_str)
- break
- def add_metadata(self):
- dialog = AddMetadataDialog(self)
- if dialog.exec() == QDialog.DialogCode.Accepted:
- key, value_type, value = dialog.get_data()
- if not key:
- QMessageBox.warning(self, "Invalid Key", "Key cannot be empty")
- return
- # Check if key already exists
- for row in range(self.metadata_table.rowCount()):
- orig_item = self.metadata_table.item(row, 0)
- if orig_item and orig_item.text() == key:
- QMessageBox.warning(self, "Duplicate Key", f"Key '{key}' already exists")
- return
- # Add to table
- row = self.metadata_table.rowCount()
- self.metadata_table.insertRow(row)
- # Key
- key_item = QTableWidgetItem(key)
- key_item.setFlags(key_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
- self.metadata_table.setItem(row, 0, key_item)
- # Type
- type_item = QTableWidgetItem(value_type.name)
- type_item.setFlags(type_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
- self.metadata_table.setItem(row, 1, type_item)
- # Value
- value_item = QTableWidgetItem(str(value))
- value_item.setFlags(value_item.flags() | Qt.ItemFlag.ItemIsEditable)
- self.metadata_table.setItem(row, 2, value_item)
- # Actions
- actions_widget = QWidget()
- actions_layout = QHBoxLayout(actions_widget)
- actions_layout.setContentsMargins(2, 2, 2, 2)
- remove_button = QPushButton("Remove")
- remove_button.setProperty("row", row)
- remove_button.setProperty("key", key)
- remove_button.clicked.connect(self.remove_metadata)
- actions_layout.addWidget(remove_button)
- self.metadata_table.setCellWidget(row, 3, actions_widget)
- # Store the change
- self.metadata_changes[key] = (value_type, value)
- self.modified = True
- self.statusBar().showMessage(f"Added new metadata key {key}")
- def save_file(self):
- if not self.reader:
- QMessageBox.warning(self, "No File Open", "Please open a GGUF file first")
- return
- if not self.modified and not self.metadata_changes and not self.metadata_to_remove:
- QMessageBox.information(self, "No Changes", "No changes to save")
- return
- file_path, _ = QFileDialog.getSaveFileName(
- self, "Save GGUF File As", "", "GGUF Files (*.gguf);;All Files (*)"
- )
- if not file_path:
- return
- try:
- self.statusBar().showMessage(f"Saving to {file_path}...")
- QApplication.processEvents()
- # Get architecture and endianness from the original file
- arch = 'unknown'
- field = self.reader.get_field(gguf.Keys.General.ARCHITECTURE)
- if field:
- arch = field.contents()
- # Create writer
- writer = GGUFWriter(file_path, arch=arch, endianess=self.reader.endianess)
- # Get alignment if present
- alignment = None
- field = self.reader.get_field(gguf.Keys.General.ALIGNMENT)
- if field:
- alignment = field.contents()
- if alignment is not None:
- writer.data_alignment = alignment
- # Copy metadata with changes
- for field in self.reader.fields.values():
- # Skip virtual fields and fields written by GGUFWriter
- if field.name == gguf.Keys.General.ARCHITECTURE or field.name.startswith('GGUF.'):
- continue
- # Skip fields marked for removal
- if field.name in self.metadata_to_remove:
- continue
- # Apply changes if any
- sub_type = None
- if field.name in self.metadata_changes:
- value_type, value = self.metadata_changes[field.name]
- if value_type == GGUFValueType.ARRAY:
- # Handle array values
- sub_type, value = value
- else:
- # Copy original value
- value = field.contents()
- value_type = field.types[0]
- if value_type == GGUFValueType.ARRAY:
- sub_type = field.types[-1]
- if value is not None:
- writer.add_key_value(field.name, value, value_type, sub_type=sub_type)
- # Add new metadata
- for key, (value_type, value) in self.metadata_changes.items():
- # Skip if the key already existed (we handled it above)
- if self.reader.get_field(key) is not None:
- continue
- sub_type = None
- if value_type == GGUFValueType.ARRAY:
- # Handle array values
- sub_type, value = value
- writer.add_key_value(key, value, value_type, sub_type=sub_type)
- # Add tensors (including data)
- for tensor in self.reader.tensors:
- writer.add_tensor(tensor.name, tensor.data, raw_shape=tensor.data.shape, raw_dtype=tensor.tensor_type, tensor_endianess=self.reader.endianess)
- # Write header and metadata
- writer.open_output_file(Path(file_path))
- writer.write_header_to_file()
- writer.write_kv_data_to_file()
- # Write tensor data using the optimized method
- writer.write_tensors_to_file(progress=False)
- writer.close()
- self.statusBar().showMessage(f"Saved to {file_path}")
- # Ask if user wants to open the new file
- reply = QMessageBox.question(
- self, "Open Saved File",
- "Would you like to open the newly saved file?",
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.Yes
- )
- if reply == QMessageBox.StandardButton.Yes:
- self.reader = GGUFReader(file_path, 'r')
- self.current_file = file_path
- self.file_path_edit.setText(file_path)
- self.load_metadata()
- self.load_tensors()
- self.metadata_changes = {}
- self.metadata_to_remove = set()
- self.modified = False
- except Exception as e:
- QMessageBox.critical(self, "Error", f"Failed to save file: {str(e)}")
- self.statusBar().showMessage("Error saving file")
- def main() -> None:
- parser = argparse.ArgumentParser(description="GUI GGUF Editor")
- parser.add_argument("model_path", nargs="?", help="path to GGUF model file to load at startup")
- parser.add_argument("--verbose", action="store_true", help="increase output verbosity")
- args = parser.parse_args()
- logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
- app = QApplication(sys.argv)
- window = GGUFEditorWindow()
- window.show()
- # Load model if specified
- if args.model_path:
- if os.path.isfile(args.model_path) and args.model_path.endswith('.gguf'):
- window.load_file(args.model_path)
- else:
- logger.error(f"Invalid model path: {args.model_path}")
- QMessageBox.warning(
- window,
- "Invalid Model Path",
- f"The specified file does not exist or is not a GGUF file: {args.model_path}")
- sys.exit(app.exec())
- if __name__ == '__main__':
- main()
|