|
46 | 46 | gguf.Keys.Tokenizer.SCORES |
47 | 47 | ] |
48 | 48 |
|
| 49 | +# Convert value based on type |
| 50 | +def parse_value(value_str: str, value_type: GGUFValueType) -> Any: |
| 51 | + if value_type == GGUFValueType.UINT8: |
| 52 | + return np.uint8(int(value_str)) |
| 53 | + elif value_type == GGUFValueType.INT8: |
| 54 | + return np.int8(int(value_str)) |
| 55 | + elif value_type == GGUFValueType.UINT16: |
| 56 | + return np.uint16(int(value_str)) |
| 57 | + elif value_type == GGUFValueType.INT16: |
| 58 | + return np.int16(int(value_str)) |
| 59 | + elif value_type == GGUFValueType.UINT32: |
| 60 | + return np.uint32(int(value_str)) |
| 61 | + elif value_type == GGUFValueType.INT32: |
| 62 | + return np.int32(int(value_str)) |
| 63 | + elif value_type == GGUFValueType.FLOAT32: |
| 64 | + return np.float32(float(value_str)) |
| 65 | + elif value_type == GGUFValueType.BOOL: |
| 66 | + return value_str.lower() in ('true', 'yes', '1') |
| 67 | + elif value_type == GGUFValueType.STRING: |
| 68 | + return value_str |
| 69 | + elif value_type == GGUFValueType.ARRAY: |
| 70 | + logger.error("Array type is not yet supported") |
| 71 | + sys.exit(1) |
| 72 | + else: |
| 73 | + return value_str |
| 74 | + |
49 | 75 |
|
50 | 76 | class TokenizerEditorDialog(QDialog): |
51 | 77 | def __init__(self, tokens, token_types, scores, parent=None): |
@@ -785,28 +811,7 @@ def get_data(self) -> Tuple[str, GGUFValueType, Any]: |
785 | 811 | key = self.key_edit.text() |
786 | 812 | value_type = self.type_combo.currentData() |
787 | 813 | value_text = self.value_edit.toPlainText() |
788 | | - |
789 | | - # Convert value based on type |
790 | | - if value_type == GGUFValueType.UINT8: |
791 | | - value = np.uint8(int(value_text)) |
792 | | - elif value_type == GGUFValueType.INT8: |
793 | | - value = np.int8(int(value_text)) |
794 | | - elif value_type == GGUFValueType.UINT16: |
795 | | - value = np.uint16(int(value_text)) |
796 | | - elif value_type == GGUFValueType.INT16: |
797 | | - value = np.int16(int(value_text)) |
798 | | - elif value_type == GGUFValueType.UINT32: |
799 | | - value = np.uint32(int(value_text)) |
800 | | - elif value_type == GGUFValueType.INT32: |
801 | | - value = np.int32(int(value_text)) |
802 | | - elif value_type == GGUFValueType.FLOAT32: |
803 | | - value = np.float32(float(value_text)) |
804 | | - elif value_type == GGUFValueType.BOOL: |
805 | | - value = value_text.lower() in ('true', 'yes', '1') |
806 | | - elif value_type == GGUFValueType.STRING: |
807 | | - value = value_text |
808 | | - else: |
809 | | - value = value_text |
| 814 | + value = parse_value(value_text, value_type) |
810 | 815 |
|
811 | 816 | return key, value_type, value |
812 | 817 |
|
@@ -1192,29 +1197,14 @@ def on_metadata_changed(self, item): |
1192 | 1197 | return |
1193 | 1198 |
|
1194 | 1199 | try: |
1195 | | - # Convert the string value to the appropriate type |
1196 | | - if value_type == GGUFValueType.UINT8: |
1197 | | - converted_value = np.uint8(int(new_value)) |
1198 | | - elif value_type == GGUFValueType.INT8: |
1199 | | - converted_value = np.int8(int(new_value)) |
1200 | | - elif value_type == GGUFValueType.UINT16: |
1201 | | - converted_value = np.uint16(int(new_value)) |
1202 | | - elif value_type == GGUFValueType.INT16: |
1203 | | - converted_value = np.int16(int(new_value)) |
1204 | | - elif value_type == GGUFValueType.UINT32: |
1205 | | - converted_value = np.uint32(int(new_value)) |
1206 | | - elif value_type == GGUFValueType.INT32: |
1207 | | - converted_value = np.int32(int(new_value)) |
1208 | | - elif value_type == GGUFValueType.FLOAT32: |
1209 | | - converted_value = np.float32(float(new_value)) |
1210 | | - elif value_type == GGUFValueType.BOOL: |
1211 | | - converted_value = new_value.lower() in ('true', 'yes', '1') |
1212 | | - elif value_type == GGUFValueType.STRING: |
1213 | | - converted_value = new_value |
1214 | | - else: |
1215 | | - # Unsupported type for editing |
| 1200 | + # Check if type is unsupported for editing first |
| 1201 | + if value_type not in (GGUFValueType.UINT8, GGUFValueType.INT8, GGUFValueType.UINT16, GGUFValueType.INT16, |
| 1202 | + GGUFValueType.UINT32, GGUFValueType.INT32, GGUFValueType.FLOAT32, GGUFValueType.BOOL, |
| 1203 | + GGUFValueType.STRING): |
1216 | 1204 | return |
1217 | 1205 |
|
| 1206 | + converted_value = parse_value(new_value, value_type) |
| 1207 | + |
1218 | 1208 | # Store the change |
1219 | 1209 | self.metadata_changes[key] = (value_type, converted_value) |
1220 | 1210 | self.modified = True |
@@ -1590,15 +1580,125 @@ def save_file(self): |
1590 | 1580 | self.statusBar().showMessage("Error saving file") |
1591 | 1581 |
|
1592 | 1582 |
|
| 1583 | +def cli_mode(args: argparse.Namespace) -> None: |
| 1584 | + logger.info(f"Loading {args.model_path}...") |
| 1585 | + reader = GGUFReader(args.model_path, 'r') |
| 1586 | + |
| 1587 | + # Validate changes |
| 1588 | + metadata_to_remove = set(args.delete) if args.delete else set() |
| 1589 | + metadata_changes = {} |
| 1590 | + |
| 1591 | + if args.set: |
| 1592 | + for key, type_str, val_str in args.set: |
| 1593 | + try: |
| 1594 | + val_type = GGUFValueType[type_str.upper()] |
| 1595 | + converted_val = parse_value(val_str, val_type) |
| 1596 | + metadata_changes[key] = (val_type, converted_val) |
| 1597 | + except KeyError: |
| 1598 | + logger.error(f"Invalid GGUF type: {type_str}") |
| 1599 | + sys.exit(1) |
| 1600 | + except ValueError as e: |
| 1601 | + logger.error(f"Failed to parse value '{val_str}' for type {type_str}: {e}") |
| 1602 | + sys.exit(1) |
| 1603 | + |
| 1604 | + if args.set_file: |
| 1605 | + for key, file_path in args.set_file: |
| 1606 | + try: |
| 1607 | + with open(file_path, 'r', encoding='utf-8') as f: |
| 1608 | + file_content = f.read() |
| 1609 | + # Explicitly default to string |
| 1610 | + metadata_changes[key] = (GGUFValueType.STRING, file_content) |
| 1611 | + except IOError as e: |
| 1612 | + logger.error(f"Failed to read from file '{file_path}': {e}") |
| 1613 | + sys.exit(1) |
| 1614 | + |
| 1615 | + logger.info(f"Saving changes to {args.output}...") |
| 1616 | + |
| 1617 | + arch = 'unknown' |
| 1618 | + arch_field = reader.get_field(gguf.Keys.General.ARCHITECTURE) |
| 1619 | + if arch_field: |
| 1620 | + arch = arch_field.contents() |
| 1621 | + |
| 1622 | + writer = GGUFWriter(args.output, arch=arch, endianess=reader.endianess) |
| 1623 | + |
| 1624 | + alignment_field = reader.get_field(gguf.Keys.General.ALIGNMENT) |
| 1625 | + if alignment_field and alignment_field.contents() is not None: |
| 1626 | + writer.data_alignment = alignment_field.contents() |
| 1627 | + |
| 1628 | + # Modify/Delete existing metadata |
| 1629 | + for field in reader.fields.values(): |
| 1630 | + if field.name == gguf.Keys.General.ARCHITECTURE or field.name.startswith('GGUF.'): |
| 1631 | + continue |
| 1632 | + if field.name in metadata_to_remove: |
| 1633 | + continue |
| 1634 | + |
| 1635 | + if field.name in metadata_changes: |
| 1636 | + val_type, value = metadata_changes[field.name] |
| 1637 | + writer.add_key_value(field.name, value, val_type) |
| 1638 | + else: |
| 1639 | + value = field.contents() |
| 1640 | + if value is not None: |
| 1641 | + sub_type = field.types[-1] if field.types[0] == GGUFValueType.ARRAY else None |
| 1642 | + writer.add_key_value(field.name, value, field.types[0], sub_type=sub_type) |
| 1643 | + |
| 1644 | + # Add new metadata |
| 1645 | + for key, (val_type, value) in metadata_changes.items(): |
| 1646 | + if reader.get_field(key) is None: |
| 1647 | + writer.add_key_value(key, value, val_type) |
| 1648 | + |
| 1649 | + # Build new tensors list |
| 1650 | + for tensor in reader.tensors: |
| 1651 | + writer.add_tensor( |
| 1652 | + tensor.name, |
| 1653 | + tensor.data, |
| 1654 | + raw_shape=tensor.data.shape, |
| 1655 | + raw_dtype=tensor.tensor_type, |
| 1656 | + tensor_endianess=reader.endianess |
| 1657 | + ) |
| 1658 | + |
| 1659 | + # Save the file |
| 1660 | + writer.open_output_file(Path(args.output)) |
| 1661 | + writer.write_header_to_file() |
| 1662 | + writer.write_kv_data_to_file() |
| 1663 | + writer.write_tensors_to_file(progress=True) |
| 1664 | + writer.close() |
| 1665 | + |
| 1666 | + logger.info("Done.") |
| 1667 | + |
| 1668 | + |
1593 | 1669 | def main() -> None: |
1594 | | - parser = argparse.ArgumentParser(description="GUI GGUF Editor") |
1595 | | - parser.add_argument("model_path", nargs="?", help="path to GGUF model file to load at startup") |
1596 | | - parser.add_argument("--verbose", action="store_true", help="increase output verbosity") |
| 1670 | + parser = argparse.ArgumentParser(description="GUI/CLI GGUF Editor") |
| 1671 | + parser.add_argument("model_path", nargs="?", help="path to GGUF model file to load") |
| 1672 | + parser.add_argument("-v", "--verbose", action="store_true", help="increase output verbosity") |
| 1673 | + |
| 1674 | + # CLI Mode Arguments |
| 1675 | + parser.add_argument("-s", "--set", nargs=3, action="append", metavar=("KEY", "TYPE", "VALUE"), |
| 1676 | + help="Set key to value. TYPE must be a valid GGUFValueType (e.g. STRING, FLOAT32)") |
| 1677 | + parser.add_argument("-f", "--set-file", nargs=2, action="append", metavar=("KEY", "FILE"), |
| 1678 | + help="Set key to string contents of file") |
| 1679 | + parser.add_argument("-d", "--delete", action="append", metavar="KEY", |
| 1680 | + help="Delete key") |
| 1681 | + parser.add_argument("-o", "--output", metavar="FILE", |
| 1682 | + help="Output file to save the modified GGUF (required if --set or --delete are used)") |
1597 | 1683 |
|
1598 | 1684 | args = parser.parse_args() |
1599 | 1685 |
|
1600 | 1686 | logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO) |
1601 | 1687 |
|
| 1688 | + # Check if CLI mode |
| 1689 | + if args.set or args.set_file or args.delete: |
| 1690 | + if not args.model_path or not args.output: |
| 1691 | + logger.error("CLI mode requires both an input 'model_path' and an '--output' file") |
| 1692 | + sys.exit(1) |
| 1693 | + if args.model_path == args.output: |
| 1694 | + logger.error("Input and output cannot be the same file") |
| 1695 | + sys.exit(1) |
| 1696 | + if not os.path.isfile(args.model_path): |
| 1697 | + logger.error(f"Invalid model path: {args.model_path}") |
| 1698 | + sys.exit(1) |
| 1699 | + cli_mode(args) |
| 1700 | + sys.exit(0) |
| 1701 | + |
1602 | 1702 | app = QApplication(sys.argv) |
1603 | 1703 | window = GGUFEditorWindow() |
1604 | 1704 | window.show() |
|
0 commit comments