Skip to content

Commit 95bd711

Browse files
committed
Add CLI options to add/modify/delete metadata values
1 parent 0929436 commit 95bd711

1 file changed

Lines changed: 146 additions & 46 deletions

File tree

gguf-py/gguf/scripts/gguf_editor_gui.py

Lines changed: 146 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,32 @@
4646
gguf.Keys.Tokenizer.SCORES
4747
]
4848

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+
4975

5076
class TokenizerEditorDialog(QDialog):
5177
def __init__(self, tokens, token_types, scores, parent=None):
@@ -785,28 +811,7 @@ def get_data(self) -> Tuple[str, GGUFValueType, Any]:
785811
key = self.key_edit.text()
786812
value_type = self.type_combo.currentData()
787813
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)
810815

811816
return key, value_type, value
812817

@@ -1192,29 +1197,14 @@ def on_metadata_changed(self, item):
11921197
return
11931198

11941199
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):
12161204
return
12171205

1206+
converted_value = parse_value(new_value, value_type)
1207+
12181208
# Store the change
12191209
self.metadata_changes[key] = (value_type, converted_value)
12201210
self.modified = True
@@ -1590,15 +1580,125 @@ def save_file(self):
15901580
self.statusBar().showMessage("Error saving file")
15911581

15921582

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+
15931669
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)")
15971683

15981684
args = parser.parse_args()
15991685

16001686
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
16011687

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+
16021702
app = QApplication(sys.argv)
16031703
window = GGUFEditorWindow()
16041704
window.show()

0 commit comments

Comments
 (0)