From 70a566b6dc4db871114b268fde37808f90c5ba6a Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 09:15:11 -0600 Subject: [PATCH 01/26] Committing gguf_editor_gui code --- gguf-py/gguf/scripts/gguf_editor_gui.py | 1658 +++++++++++++++++ requirements/requirements-gguf_editor_gui.txt | 8 + 2 files changed, 1666 insertions(+) create mode 100755 gguf-py/gguf/scripts/gguf_editor_gui.py create mode 100644 requirements/requirements-gguf_editor_gui.txt diff --git a/gguf-py/gguf/scripts/gguf_editor_gui.py b/gguf-py/gguf/scripts/gguf_editor_gui.py new file mode 100755 index 0000000000000..e9a2bc1ca1aa0 --- /dev/null +++ b/gguf-py/gguf/scripts/gguf_editor_gui.py @@ -0,0 +1,1658 @@ +#!/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 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)) + +try: + import gguf + from gguf import GGUFReader, GGUFWriter, GGUFValueType, ReaderField + from gguf.constants import TokenType, RopeScalingType, PoolingType, GGMLQuantizationType +except ImportError as e: + if "sentencepiece" in str(e): + print("Error: Missing sentencepiece module") + print("Please install it with: pip install sentencepiece") + sys.exit(1) + elif "yaml" in str(e): + print("Error: Missing PyYAML module") + print("Please install it with: pip install PyYAML") + sys.exit(1) + else: + raise + +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] + #print(f"Key: {key}; Element Type: {element_type}") + + 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.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 + try: + self.metadata_table.itemChanged.disconnect(self.on_metadata_changed) + except: + pass + + 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) + + 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, "Invalid Enum Value", + 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 = self.decode_field(field) + + # 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(f"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 = self.decode_field(field) + + # Determine endianness + if np.uint32(1) == np.uint32(1).newbyteorder("<"): + # Host is little endian + host_endian = gguf.GGUFEndian.LITTLE + swapped_endian = gguf.GGUFEndian.BIG + else: + host_endian = gguf.GGUFEndian.BIG + swapped_endian = gguf.GGUFEndian.LITTLE + + if self.reader.byte_order == "S": + endianess = swapped_endian + else: + endianess = host_endian + + # Create writer + writer = GGUFWriter(file_path, arch=arch, endianess=endianess) + + # Get alignment if present + alignment = None + field = self.reader.get_field(gguf.Keys.General.ALIGNMENT) + if field: + alignment = self.decode_field(field) + 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 + if field.name in self.metadata_changes: + value_type, value = self.metadata_changes[field.name] + if value_type == GGUFValueType.ARRAY: + # Handle array values + element_type, array_values = value + writer.add_array(field.name, array_values) + else: + writer.add_key_value(field.name, value, value_type) + else: + # Copy original value + value = self.decode_field(field) + if value is not None and field.types: + writer.add_key_value(field.name, value, field.types[0]) + + # 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 + + writer.add_key_value(key, value, value_type) + + # Copy tensors + for tensor in self.reader.tensors: + writer.add_tensor_info(tensor.name, tensor.data.shape, tensor.data.dtype, tensor.data.nbytes, tensor.tensor_type) + + # Write header and metadata + writer.write_header_to_file() + writer.write_kv_data_to_file() + writer.write_ti_data_to_file() + + # Write tensor data + for tensor in self.reader.tensors: + writer.write_tensor_data(tensor.data) + + 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 decode_field(self, field: ReaderField) -> Any: + if field and field.types: + main_type = field.types[0] + + if main_type == GGUFValueType.ARRAY: + sub_type = field.types[-1] + + if sub_type == GGUFValueType.STRING: + return [str(bytes(field.parts[idx]), encoding='utf-8') for idx in field.data] + else: + values = [pv for idx in field.data for pv in field.parts[idx].tolist()] + + # Special handling for token types + if field.name == gguf.Keys.Tokenizer.TOKEN_TYPE and sub_type == GGUFValueType.INT32: + # Return the raw values, they'll be converted to TokenType when needed + return values + return values + + if main_type == GGUFValueType.STRING: + return str(bytes(field.parts[-1]), encoding='utf-8') + else: + return field.parts[-1][0] + + return None + + +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() diff --git a/requirements/requirements-gguf_editor_gui.txt b/requirements/requirements-gguf_editor_gui.txt new file mode 100644 index 0000000000000..f99b66529f758 --- /dev/null +++ b/requirements/requirements-gguf_editor_gui.txt @@ -0,0 +1,8 @@ +numpy==2.2.4 +PySide6==6.9.0 +PySide6_Addons==6.9.0 +PySide6_Essentials==6.9.0 +PyYAML==6.0.2 +sentencepiece==0.2.0 +shiboken6==6.9.0 +tqdm==4.67.1 \ No newline at end of file From e6620de962f05b007f50188d3116e2753c58edf2 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 10:29:04 -0600 Subject: [PATCH 02/26] Revising requirements file to align with existing requirements --- requirements/requirements-gguf_editor_gui.txt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/requirements/requirements-gguf_editor_gui.txt b/requirements/requirements-gguf_editor_gui.txt index f99b66529f758..45a8e876cb899 100644 --- a/requirements/requirements-gguf_editor_gui.txt +++ b/requirements/requirements-gguf_editor_gui.txt @@ -1,8 +1,8 @@ -numpy==2.2.4 -PySide6==6.9.0 -PySide6_Addons==6.9.0 -PySide6_Essentials==6.9.0 -PyYAML==6.0.2 -sentencepiece==0.2.0 -shiboken6==6.9.0 -tqdm==4.67.1 \ No newline at end of file +numpy~=1.26.4 +PySide6~=6.9.0 +PySide6_Addons~=6.9.0 +PySide6_Essentials~=6.9.0 +PyYAML~=6.0.1 +sentencepiece~=0.2.0 +shiboken6~=6.9.0 +tqdm~=4.67.1 \ No newline at end of file From ad1c90d4320a74b2a79293017ce43ce1a3893830 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 11:36:58 -0600 Subject: [PATCH 03/26] chore: Remove try-except block for gguf import --- gguf-py/gguf/scripts/gguf_editor_gui.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/gguf-py/gguf/scripts/gguf_editor_gui.py b/gguf-py/gguf/scripts/gguf_editor_gui.py index e9a2bc1ca1aa0..d1324e08c479f 100755 --- a/gguf-py/gguf/scripts/gguf_editor_gui.py +++ b/gguf-py/gguf/scripts/gguf_editor_gui.py @@ -24,21 +24,9 @@ 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)) -try: - import gguf - from gguf import GGUFReader, GGUFWriter, GGUFValueType, ReaderField - from gguf.constants import TokenType, RopeScalingType, PoolingType, GGMLQuantizationType -except ImportError as e: - if "sentencepiece" in str(e): - print("Error: Missing sentencepiece module") - print("Please install it with: pip install sentencepiece") - sys.exit(1) - elif "yaml" in str(e): - print("Error: Missing PyYAML module") - print("Please install it with: pip install PyYAML") - sys.exit(1) - else: - raise +import gguf +from gguf import GGUFReader, GGUFWriter, GGUFValueType, ReaderField +from gguf.constants import TokenType, RopeScalingType, PoolingType, GGMLQuantizationType logger = logging.getLogger("gguf-editor-gui") From 7f52f2f7ae5afebb11c73a62bf5f0c30bd5d7a53 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 11:50:37 -0600 Subject: [PATCH 04/26] Updating .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2c67ad7f7c609..3b174a6068aa0 100644 --- a/.gitignore +++ b/.gitignore @@ -146,3 +146,4 @@ poetry.toml # Local scripts /run-vim.sh /run-chat.sh +.aider* From 029314a63e1811fda4614bfb862efb77c63f6b02 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 12:44:38 -0600 Subject: [PATCH 05/26] Implementing review changes --- gguf-py/gguf/scripts/gguf_editor_gui.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/gguf-py/gguf/scripts/gguf_editor_gui.py b/gguf-py/gguf/scripts/gguf_editor_gui.py index d1324e08c479f..781b86ddb9fd8 100755 --- a/gguf-py/gguf/scripts/gguf_editor_gui.py +++ b/gguf-py/gguf/scripts/gguf_editor_gui.py @@ -1551,18 +1551,17 @@ def save_file(self): writer.add_key_value(key, value, value_type) - # Copy tensors + # Add tensors (including data) for tensor in self.reader.tensors: - writer.add_tensor_info(tensor.name, tensor.data.shape, tensor.data.dtype, tensor.data.nbytes, tensor.tensor_type) + writer.add_tensor(tensor.name, tensor.data, raw_shape=tensor.data.shape, raw_dtype=tensor.tensor_type) # Write header and metadata + writer.open_output_file(Path(file_path)) writer.write_header_to_file() writer.write_kv_data_to_file() - writer.write_ti_data_to_file() - # Write tensor data - for tensor in self.reader.tensors: - writer.write_tensor_data(tensor.data) + # Write tensor data using the optimized method + writer.write_tensors_to_file(progress=False) writer.close() From 1d304c9dedef7087bd6632c68763dda3791b6fb9 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 13:03:33 -0600 Subject: [PATCH 06/26] Addressing linting issues --- gguf-py/gguf/scripts/gguf_editor_gui.py | 670 +++++++++--------- requirements/requirements-gguf_editor_gui.txt | 2 +- 2 files changed, 339 insertions(+), 333 deletions(-) diff --git a/gguf-py/gguf/scripts/gguf_editor_gui.py b/gguf-py/gguf/scripts/gguf_editor_gui.py index 781b86ddb9fd8..15521b3c1a67f 100755 --- a/gguf-py/gguf/scripts/gguf_editor_gui.py +++ b/gguf-py/gguf/scripts/gguf_editor_gui.py @@ -12,9 +12,9 @@ import numpy as np from PySide6.QtWidgets import ( - QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, - QPushButton, QLabel, QLineEdit, QFileDialog, QTableWidget, - QTableWidgetItem, QComboBox, QMessageBox, QTabWidget, + QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QPushButton, QLabel, QLineEdit, QFileDialog, QTableWidget, + QTableWidgetItem, QComboBox, QMessageBox, QTabWidget, QTextEdit, QFormLayout, QHeaderView, QDialog, QDialogButtonBox ) @@ -45,16 +45,17 @@ 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: @@ -63,9 +64,9 @@ def __init__(self, tokens, token_types, scores, parent=None): 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:")) @@ -73,25 +74,25 @@ def __init__(self, tokens, token_types, scores, parent=None): 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) @@ -100,40 +101,40 @@ def __init__(self, tokens, token_types, scores, parent=None): 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))) @@ -143,51 +144,51 @@ def apply_filter(self): 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: @@ -195,22 +196,22 @@ def load_page(self): 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 @@ -218,20 +219,20 @@ def handle_cell_double_click(self, row, column): 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): @@ -239,53 +240,53 @@ def edit_token_type(self, row, orig_idx): 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 = [] @@ -293,10 +294,10 @@ def remove_selected(self): 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: @@ -304,7 +305,7 @@ def remove_selected(self): 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): @@ -313,23 +314,23 @@ def remove_selected(self): 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 @@ -340,33 +341,32 @@ 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] - #print(f"Key: {key}; Element Type: {element_type}") - + 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:")) @@ -374,28 +374,28 @@ def __init__(self, array_values, element_type, key=None, parent=None): 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) @@ -408,46 +408,46 @@ def __init__(self, array_values, element_type, key=None, parent=None): 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))) @@ -470,48 +470,48 @@ def apply_filter(self): # 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 @@ -523,35 +523,35 @@ def load_page(self): 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) @@ -561,44 +561,44 @@ def edit_array_enum_value(self): # 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) @@ -606,15 +606,15 @@ def bulk_edit_selected(self): 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 @@ -625,30 +625,30 @@ def add_item(self): 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: @@ -656,15 +656,15 @@ def remove_selected(self): 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) @@ -682,39 +682,39 @@ def remove_selected(self): 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) + 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): @@ -722,30 +722,30 @@ def edit_enum_value(self, row: int, enum_type: Type[enum.Enum]): 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 @@ -756,35 +756,35 @@ 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)) @@ -806,55 +806,55 @@ def get_data(self) -> Tuple[str, GGUFValueType, Any]: 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.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) @@ -864,22 +864,22 @@ def setup_ui(self): 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)"]) @@ -889,70 +889,72 @@ def setup_ui(self): 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 try: self.metadata_table.itemChanged.disconnect(self.on_metadata_changed) - except: + except (RuntimeError, TypeError): pass - + finally: + pass + 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" @@ -970,28 +972,28 @@ def load_metadata(self): 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") @@ -999,7 +1001,7 @@ def load_metadata(self): 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") @@ -1010,27 +1012,27 @@ def load_metadata(self): 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) - + 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') @@ -1038,13 +1040,13 @@ def extract_array_values(self, field: ReaderField) -> list: 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: @@ -1054,11 +1056,11 @@ def format_enum_value(self, value: Any, enum_type: Type[enum.Enum]) -> str: 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: @@ -1070,79 +1072,79 @@ def format_field_value(self, field: ReaderField) -> str: 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: @@ -1161,30 +1163,32 @@ def on_metadata_changed(self, item): 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, "Invalid Enum Value", - f"'{new_value}' is not a valid {enum_type.__name__} value.\n" - f"Valid values are: {', '.join(v.name for v in enum_type)}") - + 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: @@ -1208,69 +1212,69 @@ def on_metadata_changed(self, item): 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", + 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 = self.decode_field(field) - + # 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)): @@ -1278,62 +1282,62 @@ def edit_metadata_enum(self): 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: @@ -1343,24 +1347,24 @@ def edit_array_metadata(self): 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] @@ -1368,40 +1372,40 @@ def edit_tokenizer_metadata(self, trigger_key): _, (_, 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, + GGUFValueType.ARRAY, (tokens_field.types[1], new_tokens) ) - + if token_types_field: self.metadata_changes[gguf.Keys.Tokenizer.TOKEN_TYPE] = ( - GGUFValueType.ARRAY, + GGUFValueType.ARRAY, (token_types_field.types[1], new_token_types) ) - + if scores_field: self.metadata_changes[gguf.Keys.Tokenizer.SCORES] = ( - GGUFValueType.ARRAY, + 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(f"Updated tokenizer data") - + + 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()): @@ -1412,87 +1416,87 @@ def update_tokenizer_display(self, key, values): 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 = self.decode_field(field) - + # Determine endianness if np.uint32(1) == np.uint32(1).newbyteorder("<"): # Host is little endian @@ -1501,15 +1505,15 @@ def save_file(self): else: host_endian = gguf.GGUFEndian.BIG swapped_endian = gguf.GGUFEndian.LITTLE - + if self.reader.byte_order == "S": endianess = swapped_endian else: endianess = host_endian - + # Create writer writer = GGUFWriter(file_path, arch=arch, endianess=endianess) - + # Get alignment if present alignment = None field = self.reader.get_field(gguf.Keys.General.ALIGNMENT) @@ -1517,17 +1521,17 @@ def save_file(self): alignment = self.decode_field(field) 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 if field.name in self.metadata_changes: value_type, value = self.metadata_changes[field.name] @@ -1542,77 +1546,77 @@ def save_file(self): value = self.decode_field(field) if value is not None and field.types: writer.add_key_value(field.name, value, field.types[0]) - + # 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 - + writer.add_key_value(key, value, value_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) - + # 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", + 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 decode_field(self, field: ReaderField) -> Any: if field and field.types: main_type = field.types[0] - + if main_type == GGUFValueType.ARRAY: sub_type = field.types[-1] - + if sub_type == GGUFValueType.STRING: return [str(bytes(field.parts[idx]), encoding='utf-8') for idx in field.data] else: values = [pv for idx in field.data for pv in field.parts[idx].tolist()] - + # Special handling for token types if field.name == gguf.Keys.Tokenizer.TOKEN_TYPE and sub_type == GGUFValueType.INT32: # Return the raw values, they'll be converted to TokenType when needed return values return values - + if main_type == GGUFValueType.STRING: return str(bytes(field.parts[-1]), encoding='utf-8') else: return field.parts[-1][0] - + return None @@ -1620,24 +1624,26 @@ 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}") - + 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()) diff --git a/requirements/requirements-gguf_editor_gui.txt b/requirements/requirements-gguf_editor_gui.txt index 45a8e876cb899..a44b479d3acfa 100644 --- a/requirements/requirements-gguf_editor_gui.txt +++ b/requirements/requirements-gguf_editor_gui.txt @@ -5,4 +5,4 @@ PySide6_Essentials~=6.9.0 PyYAML~=6.0.1 sentencepiece~=0.2.0 shiboken6~=6.9.0 -tqdm~=4.67.1 \ No newline at end of file +tqdm~=4.67.1 From 04d81a8fdeb05757f15084d1c8d765181398bb07 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 13:23:43 -0600 Subject: [PATCH 07/26] Update gguf-py/gguf/scripts/gguf_editor_gui.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sigbjørn Skjæret --- gguf-py/gguf/scripts/gguf_editor_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gguf-py/gguf/scripts/gguf_editor_gui.py b/gguf-py/gguf/scripts/gguf_editor_gui.py index 15521b3c1a67f..c986085ca1497 100755 --- a/gguf-py/gguf/scripts/gguf_editor_gui.py +++ b/gguf-py/gguf/scripts/gguf_editor_gui.py @@ -1264,7 +1264,7 @@ def edit_metadata_enum(self): return # Get current value - current_value = self.decode_field(field) + current_value = field.contents() # Create a dialog with enum options dialog = QDialog(self) From 2628695b0fe0b2348a1c092913eb51c19953bd55 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 13:23:55 -0600 Subject: [PATCH 08/26] Update gguf-py/gguf/scripts/gguf_editor_gui.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sigbjørn Skjæret --- gguf-py/gguf/scripts/gguf_editor_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gguf-py/gguf/scripts/gguf_editor_gui.py b/gguf-py/gguf/scripts/gguf_editor_gui.py index c986085ca1497..cee11aff5101f 100755 --- a/gguf-py/gguf/scripts/gguf_editor_gui.py +++ b/gguf-py/gguf/scripts/gguf_editor_gui.py @@ -1495,7 +1495,7 @@ def save_file(self): arch = 'unknown' field = self.reader.get_field(gguf.Keys.General.ARCHITECTURE) if field: - arch = self.decode_field(field) + arch = field.contents() # Determine endianness if np.uint32(1) == np.uint32(1).newbyteorder("<"): From 1fa906713d9e0b9f8d31fda39d04adadd040a51d Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 13:24:04 -0600 Subject: [PATCH 09/26] Update gguf-py/gguf/scripts/gguf_editor_gui.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sigbjørn Skjæret --- gguf-py/gguf/scripts/gguf_editor_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gguf-py/gguf/scripts/gguf_editor_gui.py b/gguf-py/gguf/scripts/gguf_editor_gui.py index cee11aff5101f..9c1a36eff8c97 100755 --- a/gguf-py/gguf/scripts/gguf_editor_gui.py +++ b/gguf-py/gguf/scripts/gguf_editor_gui.py @@ -1543,7 +1543,7 @@ def save_file(self): writer.add_key_value(field.name, value, value_type) else: # Copy original value - value = self.decode_field(field) + value = field.contents() if value is not None and field.types: writer.add_key_value(field.name, value, field.types[0]) From 0675b49682870340449a62e4a3af090d8a24b1ef Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 13:24:27 -0600 Subject: [PATCH 10/26] Update gguf-py/gguf/scripts/gguf_editor_gui.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sigbjørn Skjæret --- gguf-py/gguf/scripts/gguf_editor_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gguf-py/gguf/scripts/gguf_editor_gui.py b/gguf-py/gguf/scripts/gguf_editor_gui.py index 9c1a36eff8c97..ab717c7962a74 100755 --- a/gguf-py/gguf/scripts/gguf_editor_gui.py +++ b/gguf-py/gguf/scripts/gguf_editor_gui.py @@ -1518,7 +1518,7 @@ def save_file(self): alignment = None field = self.reader.get_field(gguf.Keys.General.ALIGNMENT) if field: - alignment = self.decode_field(field) + alignment = field.contents() if alignment is not None: writer.data_alignment = alignment From 3f0f32c67985746dcd3dc5d024a1d47d61ec5d98 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 13:28:58 -0600 Subject: [PATCH 11/26] =?UTF-8?q?Applying=20@CISC=20(Sigbj=C3=B8rn=20Skj?= =?UTF-8?q?=C3=A6ret)'s=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gguf-py/gguf/scripts/gguf_editor_gui.py | 42 ++----------------------- 1 file changed, 2 insertions(+), 40 deletions(-) diff --git a/gguf-py/gguf/scripts/gguf_editor_gui.py b/gguf-py/gguf/scripts/gguf_editor_gui.py index ab717c7962a74..e99b897a04940 100755 --- a/gguf-py/gguf/scripts/gguf_editor_gui.py +++ b/gguf-py/gguf/scripts/gguf_editor_gui.py @@ -1497,22 +1497,9 @@ def save_file(self): if field: arch = field.contents() - # Determine endianness - if np.uint32(1) == np.uint32(1).newbyteorder("<"): - # Host is little endian - host_endian = gguf.GGUFEndian.LITTLE - swapped_endian = gguf.GGUFEndian.BIG - else: - host_endian = gguf.GGUFEndian.BIG - swapped_endian = gguf.GGUFEndian.LITTLE - - if self.reader.byte_order == "S": - endianess = swapped_endian - else: - endianess = host_endian - # Create writer - writer = GGUFWriter(file_path, arch=arch, endianess=endianess) + writer = GGUFWriter(file_path, arch=arch, endianess=self.reader.endianess) + # Get alignment if present alignment = None @@ -1594,31 +1581,6 @@ def save_file(self): QMessageBox.critical(self, "Error", f"Failed to save file: {str(e)}") self.statusBar().showMessage("Error saving file") - def decode_field(self, field: ReaderField) -> Any: - if field and field.types: - main_type = field.types[0] - - if main_type == GGUFValueType.ARRAY: - sub_type = field.types[-1] - - if sub_type == GGUFValueType.STRING: - return [str(bytes(field.parts[idx]), encoding='utf-8') for idx in field.data] - else: - values = [pv for idx in field.data for pv in field.parts[idx].tolist()] - - # Special handling for token types - if field.name == gguf.Keys.Tokenizer.TOKEN_TYPE and sub_type == GGUFValueType.INT32: - # Return the raw values, they'll be converted to TokenType when needed - return values - return values - - if main_type == GGUFValueType.STRING: - return str(bytes(field.parts[-1]), encoding='utf-8') - else: - return field.parts[-1][0] - - return None - def main() -> None: parser = argparse.ArgumentParser(description="GUI GGUF Editor") From 926804121be495a7340c7af108031a595833d898 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 14:15:12 -0600 Subject: [PATCH 12/26] Update requirements/requirements-gguf_editor_gui.txt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sigbjørn Skjæret --- requirements/requirements-gguf_editor_gui.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/requirements/requirements-gguf_editor_gui.txt b/requirements/requirements-gguf_editor_gui.txt index a44b479d3acfa..f38b8d546c34b 100644 --- a/requirements/requirements-gguf_editor_gui.txt +++ b/requirements/requirements-gguf_editor_gui.txt @@ -1,8 +1,5 @@ numpy~=1.26.4 PySide6~=6.9.0 -PySide6_Addons~=6.9.0 -PySide6_Essentials~=6.9.0 PyYAML~=6.0.1 sentencepiece~=0.2.0 -shiboken6~=6.9.0 tqdm~=4.67.1 From af8d354fe968349ec56a76b9e3dfdf083586ec31 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 14:15:53 -0600 Subject: [PATCH 13/26] Update gguf-py/gguf/scripts/gguf_editor_gui.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sigbjørn Skjæret --- gguf-py/gguf/scripts/gguf_editor_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gguf-py/gguf/scripts/gguf_editor_gui.py b/gguf-py/gguf/scripts/gguf_editor_gui.py index e99b897a04940..1a48e4bd428dd 100755 --- a/gguf-py/gguf/scripts/gguf_editor_gui.py +++ b/gguf-py/gguf/scripts/gguf_editor_gui.py @@ -248,7 +248,7 @@ def edit_token_type(self, row, orig_idx): buttons.rejected.connect(dialog.reject) layout.addWidget(buttons) - if dialog.exec_() == QDialog.DialogCode.Accepted: + if dialog.exec() == QDialog.DialogCode.Accepted: # Get the selected value new_value = combo.currentData() enum_val = TokenType(new_value) From cf0f6fcca73598aafb45e495ae0dabeb39b18088 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 14:16:03 -0600 Subject: [PATCH 14/26] Update gguf-py/gguf/scripts/gguf_editor_gui.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sigbjørn Skjæret --- gguf-py/gguf/scripts/gguf_editor_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gguf-py/gguf/scripts/gguf_editor_gui.py b/gguf-py/gguf/scripts/gguf_editor_gui.py index 1a48e4bd428dd..5d38d3fe909d1 100755 --- a/gguf-py/gguf/scripts/gguf_editor_gui.py +++ b/gguf-py/gguf/scripts/gguf_editor_gui.py @@ -593,7 +593,7 @@ def bulk_edit_selected(self): buttons.rejected.connect(dialog.reject) layout.addWidget(buttons) - if dialog.exec_() == QDialog.DialogCode.Accepted: + if dialog.exec() == QDialog.DialogCode.Accepted: # Get the selected value new_value = combo.currentData() enum_val = self.enum_type(new_value) From b6df560b17a02efcc5630179b8910d933a2e56c2 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 14:16:56 -0600 Subject: [PATCH 15/26] Update gguf-py/gguf/scripts/gguf_editor_gui.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sigbjørn Skjæret --- gguf-py/gguf/scripts/gguf_editor_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gguf-py/gguf/scripts/gguf_editor_gui.py b/gguf-py/gguf/scripts/gguf_editor_gui.py index 5d38d3fe909d1..3ef1c5a723393 100755 --- a/gguf-py/gguf/scripts/gguf_editor_gui.py +++ b/gguf-py/gguf/scripts/gguf_editor_gui.py @@ -730,7 +730,7 @@ def edit_enum_value(self, row: int, enum_type: Type[enum.Enum]): buttons.rejected.connect(dialog.reject) layout.addWidget(buttons) - if dialog.exec_() == QDialog.DialogCode.Accepted: + if dialog.exec() == QDialog.DialogCode.Accepted: # Update the value display and stored data new_value = combo.currentData() enum_val = enum_type(new_value) From 0349aa998409f7d1993166a455ca7ed80f35cda9 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 14:17:05 -0600 Subject: [PATCH 16/26] Update gguf-py/gguf/scripts/gguf_editor_gui.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sigbjørn Skjæret --- gguf-py/gguf/scripts/gguf_editor_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gguf-py/gguf/scripts/gguf_editor_gui.py b/gguf-py/gguf/scripts/gguf_editor_gui.py index 3ef1c5a723393..70da55dc85c5a 100755 --- a/gguf-py/gguf/scripts/gguf_editor_gui.py +++ b/gguf-py/gguf/scripts/gguf_editor_gui.py @@ -1290,7 +1290,7 @@ def edit_metadata_enum(self): buttons.rejected.connect(dialog.reject) layout.addWidget(buttons) - if dialog.exec_() == QDialog.DialogCode.Accepted: + if dialog.exec() == QDialog.DialogCode.Accepted: # Get the selected value new_value = combo.currentData() enum_val = enum_type(new_value) From b021fd90b6260545fa7aaad7c19133f633bcd87c Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 14:17:28 -0600 Subject: [PATCH 17/26] Update gguf-py/gguf/scripts/gguf_editor_gui.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sigbjørn Skjæret --- gguf-py/gguf/scripts/gguf_editor_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gguf-py/gguf/scripts/gguf_editor_gui.py b/gguf-py/gguf/scripts/gguf_editor_gui.py index 70da55dc85c5a..c6f155951a7c8 100755 --- a/gguf-py/gguf/scripts/gguf_editor_gui.py +++ b/gguf-py/gguf/scripts/gguf_editor_gui.py @@ -1331,7 +1331,7 @@ def edit_array_metadata(self): # Open array editor dialog dialog = ArrayEditorDialog(array_values, element_type, key, self) - if dialog.exec_() == QDialog.DialogCode.Accepted: + if dialog.exec() == QDialog.DialogCode.Accepted: new_values = dialog.get_array_values() # Store the change From 229da6949d086e89dbf391e75c1c5af875456c56 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 14:17:35 -0600 Subject: [PATCH 18/26] Update gguf-py/gguf/scripts/gguf_editor_gui.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sigbjørn Skjæret --- gguf-py/gguf/scripts/gguf_editor_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gguf-py/gguf/scripts/gguf_editor_gui.py b/gguf-py/gguf/scripts/gguf_editor_gui.py index c6f155951a7c8..66c96bc844a3e 100755 --- a/gguf-py/gguf/scripts/gguf_editor_gui.py +++ b/gguf-py/gguf/scripts/gguf_editor_gui.py @@ -1375,7 +1375,7 @@ def edit_tokenizer_metadata(self, trigger_key): # Open the tokenizer editor dialog dialog = TokenizerEditorDialog(tokens, token_types, scores, self) - if dialog.exec_() == QDialog.DialogCode.Accepted: + if dialog.exec() == QDialog.DialogCode.Accepted: new_tokens, new_token_types, new_scores = dialog.get_data() # Store changes for all three arrays From 34234502ba4f2a89bda04f70771fc585b772b887 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 14:17:43 -0600 Subject: [PATCH 19/26] Update gguf-py/gguf/scripts/gguf_editor_gui.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sigbjørn Skjæret --- gguf-py/gguf/scripts/gguf_editor_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gguf-py/gguf/scripts/gguf_editor_gui.py b/gguf-py/gguf/scripts/gguf_editor_gui.py index 66c96bc844a3e..ee327ecbfe6bd 100755 --- a/gguf-py/gguf/scripts/gguf_editor_gui.py +++ b/gguf-py/gguf/scripts/gguf_editor_gui.py @@ -1419,7 +1419,7 @@ def update_tokenizer_display(self, key, values): def add_metadata(self): dialog = AddMetadataDialog(self) - if dialog.exec_() == QDialog.DialogCode.Accepted: + if dialog.exec() == QDialog.DialogCode.Accepted: key, value_type, value = dialog.get_data() if not key: From ad607a3e0bf02616aee293acc337df9c1b1ab5dc Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 14:48:10 -0600 Subject: [PATCH 20/26] Filtering warnings during loading --- gguf-py/gguf/scripts/gguf_editor_gui.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/gguf-py/gguf/scripts/gguf_editor_gui.py b/gguf-py/gguf/scripts/gguf_editor_gui.py index ee327ecbfe6bd..4ed0466a3bac5 100755 --- a/gguf-py/gguf/scripts/gguf_editor_gui.py +++ b/gguf-py/gguf/scripts/gguf_editor_gui.py @@ -9,6 +9,7 @@ import enum from pathlib import Path from typing import Any, Optional, Tuple, Type +import warnings import numpy as np from PySide6.QtWidgets import ( @@ -940,12 +941,9 @@ def load_metadata(self): return # Disconnect to prevent triggering during loading - try: + with warnings.catch_warnings(): + warnings.filterwarnings('ignore') self.metadata_table.itemChanged.disconnect(self.on_metadata_changed) - except (RuntimeError, TypeError): - pass - finally: - pass for i, (key, field) in enumerate(self.reader.fields.items()): self.metadata_table.insertRow(i) From bf1be33dc49f1a913c08045324aa2c81e542a46e Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 13 Apr 2025 15:03:12 -0600 Subject: [PATCH 21/26] Fixing linting issue --- gguf-py/gguf/scripts/gguf_editor_gui.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gguf-py/gguf/scripts/gguf_editor_gui.py b/gguf-py/gguf/scripts/gguf_editor_gui.py index 4ed0466a3bac5..9dab6ca276e47 100755 --- a/gguf-py/gguf/scripts/gguf_editor_gui.py +++ b/gguf-py/gguf/scripts/gguf_editor_gui.py @@ -1498,7 +1498,6 @@ def save_file(self): # 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) From 42bd4ffac83403c0c68e4f4b357fe843185c64da Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Wed, 16 Apr 2025 22:15:39 -0600 Subject: [PATCH 22/26] Enabling an optional gui extra for installation --- gguf-py/README.md | 7 +++++++ gguf-py/gguf/scripts/__init__.py | 1 + gguf-py/pyproject.toml | 7 ++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/gguf-py/README.md b/gguf-py/README.md index dd4ab7bde763a..ca7e09c68184f 100644 --- a/gguf-py/README.md +++ b/gguf-py/README.md @@ -11,6 +11,11 @@ as an example for its usage. pip install gguf ``` +Optionally, you can install gguf with the extra 'gui' to enable the visual GGUF editor. +```sh +pip install gguf[gui] +``` + ## API Examples/Simple Tools [examples/writer.py](https://github.com/ggml-org/llama.cpp/blob/master/gguf-py/examples/writer.py) — Generates `example.gguf` in the current directory to demonstrate generating a GGUF file. Note that this file cannot be used as a model. @@ -25,6 +30,8 @@ pip install gguf [gguf/scripts/gguf_new_metadata.py](https://github.com/ggml-org/llama.cpp/blob/master/gguf-py/gguf/scripts/gguf_new_metadata.py) — Copies a GGUF file with added/modified/removed metadata values. +[gguf/scripts/gguf_editor_gui.py](https://github.com/ggml-org/llama.cpp/blob/master/gguf-py/gguf/scripts/gguf_editor_gui.py) — Allows for viewing, editing, adding, or removing metadata values within a GGUF file as well as viewing its tensors with a Qt interface. + ## Development Maintainers who participate in development of this package are advised to install it in editable mode: diff --git a/gguf-py/gguf/scripts/__init__.py b/gguf-py/gguf/scripts/__init__.py index e77f2e9c97c31..72cc73e700a6d 100644 --- a/gguf-py/gguf/scripts/__init__.py +++ b/gguf-py/gguf/scripts/__init__.py @@ -4,3 +4,4 @@ from .gguf_dump import main as gguf_dump_entrypoint from .gguf_set_metadata import main as gguf_set_metadata_entrypoint from .gguf_new_metadata import main as gguf_new_metadata_entrypoint +from .gguf_editor_gui import main as gguf_editor_gui_entrypoint diff --git a/gguf-py/pyproject.toml b/gguf-py/pyproject.toml index d214e8720122b..9745d3ea84786 100644 --- a/gguf-py/pyproject.toml +++ b/gguf-py/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "gguf" -version = "0.16.0" +version = "0.16.1" description = "Read and write ML models in GGUF for GGML" authors = ["GGML "] packages = [ @@ -23,10 +23,14 @@ numpy = ">=1.17" tqdm = ">=4.27" pyyaml = ">=5.1" sentencepiece = ">=0.1.98,<=0.2.0" +PySide6 = { version = "^6.9", optional = true } [tool.poetry.dev-dependencies] pytest = "^5.2" +[tool.poetry.extras] +gui = ["PySide6"] + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" @@ -36,3 +40,4 @@ gguf-convert-endian = "gguf.scripts:gguf_convert_endian_entrypoint" gguf-dump = "gguf.scripts:gguf_dump_entrypoint" gguf-set-metadata = "gguf.scripts:gguf_set_metadata_entrypoint" gguf-new-metadata = "gguf.scripts:gguf_new_metadata_entrypoint" +gguf-editor-gui = "gguf.scripts:gguf_editor_gui_entrypoint" From a1e2c5d4e586d9fe862780e761f4067c26180c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigbj=C3=B8rn=20Skj=C3=A6ret?= Date: Fri, 18 Apr 2025 13:42:36 +0200 Subject: [PATCH 23/26] probably useful, but does not belong in this PR --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3b174a6068aa0..2c67ad7f7c609 100644 --- a/.gitignore +++ b/.gitignore @@ -146,4 +146,3 @@ poetry.toml # Local scripts /run-vim.sh /run-chat.sh -.aider* From a7c9cd8fba70f6d65eaf1cb38253868894effd04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigbj=C3=B8rn=20Skj=C3=A6ret?= Date: Fri, 18 Apr 2025 14:07:46 +0200 Subject: [PATCH 24/26] narrow down requirements --- requirements/requirements-gguf_editor_gui.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/requirements/requirements-gguf_editor_gui.txt b/requirements/requirements-gguf_editor_gui.txt index f38b8d546c34b..03ced25656f41 100644 --- a/requirements/requirements-gguf_editor_gui.txt +++ b/requirements/requirements-gguf_editor_gui.txt @@ -1,5 +1,2 @@ numpy~=1.26.4 PySide6~=6.9.0 -PyYAML~=6.0.1 -sentencepiece~=0.2.0 -tqdm~=4.67.1 From dd18eaba36fc2e45bca70bf0a322c7f0b540a282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigbj=C3=B8rn=20Skj=C3=A6ret?= Date: Fri, 18 Apr 2025 14:09:03 +0200 Subject: [PATCH 25/26] add gguf editor gui requirements --- requirements/requirements-all.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements/requirements-all.txt b/requirements/requirements-all.txt index 439db888636dc..eba0a59f62fe3 100644 --- a/requirements/requirements-all.txt +++ b/requirements/requirements-all.txt @@ -11,3 +11,5 @@ -r ./requirements-convert_legacy_llama.txt -r ./requirements-convert_llama_ggml_to_gguf.txt -r ./requirements-tool_bench.txt + +-r ./requirements-gguf_editor_gui.txt From e82156318e6a43092181e937ff3b408b4d56da5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigbj=C3=B8rn=20Skj=C3=A6ret?= Date: Fri, 18 Apr 2025 20:14:58 +0200 Subject: [PATCH 26/26] add gguf dependency --- requirements/requirements-gguf_editor_gui.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/requirements-gguf_editor_gui.txt b/requirements/requirements-gguf_editor_gui.txt index 03ced25656f41..920dc7cf90b94 100644 --- a/requirements/requirements-gguf_editor_gui.txt +++ b/requirements/requirements-gguf_editor_gui.txt @@ -1,2 +1,3 @@ numpy~=1.26.4 PySide6~=6.9.0 +gguf>=0.16.0