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