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