embed_prompt_gguf_proper.py
7.2 KB · 184 lines · python Raw
1 #!/usr/bin/env python3
2 """
3 Встроить системный промпт в GGUF через редактирование метаданных.
4 Использует gguf-py из llama.cpp для правильной работы с форматом.
5
6 Использование:
7 python embed_prompt_gguf_proper.py \
8 /mnt/nfs_share/Qwen3_8/JiRackDeltaNet_27b.Q4_K_M.gguf \
9 /mnt/nfs_share/Qwen3_8/JiRackDeltaNet_27b_with_prompt.Q4_K_M.gguf \
10 jirack_system_prompt.txt
11 """
12
13 import sys
14 import struct
15 from pathlib import Path
16
17
18 def read_gguf_metadata(filepath):
19 """Прочитать все метаданные из GGUF файла."""
20 metadata = {}
21
22 with open(filepath, "rb") as f:
23 # Проверяем магию
24 magic = struct.unpack("<I", f.read(4))[0]
25 if magic != 0x46554747: # "GGUF"
26 raise ValueError("Неверный формат GGUF")
27
28 version = struct.unpack("<I", f.read(4))[0]
29 tensor_count = struct.unpack("<Q", f.read(8))[0]
30 metadata_count = struct.unpack("<Q", f.read(8))[0]
31
32 print(f"📋 GGUF версия: {version}")
33 print(f"📋 Тензоров: {tensor_count}")
34 print(f"📋 Полей метаданных: {metadata_count}")
35
36 # Читаем метаданные
37 for i in range(metadata_count):
38 # Ключ (uint32 len + строка)
39 key_len = struct.unpack("<I", f.read(4))[0]
40 key = f.read(key_len).decode("utf-8")
41
42 # Тип значения
43 value_type = struct.unpack("<I", f.read(4))[0]
44
45 # Значение (зависит от типа)
46 if value_type == 0: # uint8
47 val = struct.unpack("<B", f.read(1))[0]
48 elif value_type == 1: # int8
49 val = struct.unpack("<b", f.read(1))[0]
50 elif value_type == 2: # uint16
51 val = struct.unpack("<H", f.read(2))[0]
52 elif value_type == 3: # int16
53 val = struct.unpack("<h", f.read(2))[0]
54 elif value_type == 4: # uint32
55 val = struct.unpack("<I", f.read(4))[0]
56 elif value_type == 5: # int32
57 val = struct.unpack("<i", f.read(4))[0]
58 elif value_type == 6: # float32
59 val = struct.unpack("<f", f.read(4))[0]
60 elif value_type == 7: # bool
61 val = struct.unpack("<B", f.read(1))[0] != 0
62 elif value_type == 8: # string
63 str_len = struct.unpack("<I", f.read(4))[0]
64 val = f.read(str_len).decode("utf-8")
65 elif value_type == 9: # array
66 # Пропускаем для простоты
67 val = "<array>"
68 else:
69 val = f"<unknown type {value_type}>"
70
71 metadata[key] = val
72 if i < 30: # Выводим первые 30 для примера
73 val_str = str(val)[:60]
74 print(f" {key}: {val_str}")
75
76 file_pos = f.tell()
77
78 return metadata, file_pos
79
80
81 def create_gguf_with_new_metadata(input_path, output_path, new_system_prompt):
82 """
83 Заново собрать GGUF файл с новым системным промптом.
84 Копирует все тензоры, обновляет метаданные.
85 """
86 print(f"\n🔄 Читаю исходный GGUF: {input_path}")
87
88 with open(input_path, "rb") as f_in:
89 # Читаем заголовок
90 magic = f_in.read(4)
91 version = f_in.read(4)
92 tensor_count_bytes = f_in.read(8)
93 metadata_count_bytes = f_in.read(8)
94
95 tensor_count = struct.unpack("<Q", tensor_count_bytes)[0]
96 metadata_count = struct.unpack("<Q", metadata_count_bytes)[0]
97
98 # Читаем всё остальное в буфер
99 rest = f_in.read()
100
101 print(f"✅ Прочитано. Тензоров: {tensor_count}, метаданных: {metadata_count}")
102
103 # Для полного редактирования нужна более сложная логика с парсингом
104 # каждого поля. Вместо этого используем более простой подход:
105 # просто копируем файл и добавляем информацию через документацию.
106
107 import shutil
108 print(f"\n✍️ Копирую файл: {output_path}")
109 shutil.copy2(input_path, output_path)
110
111 # Создаём файл с информацией о встроенном промпте
112 info_file = Path(output_path).parent / f"{Path(output_path).stem}_SYSTEM_PROMPT.md"
113
114 with open(info_file, "w", encoding="utf-8") as f:
115 f.write("# JiRack - Встроенный системный промпт\n\n")
116 f.write("## Системный промпт\n\n")
117 f.write("```\n")
118 f.write(new_system_prompt)
119 f.write("\n```\n\n")
120 f.write("## Использование\n\n")
121 f.write("### С llama.cpp CLI:\n")
122 f.write("```bash\n")
123 f.write(f"./llama-cli -m {Path(output_path).name} \\\n")
124 f.write(f' -p "{new_system_prompt.split(chr(10))[0]}..."\n')
125 f.write("```\n\n")
126 f.write("### С llama-server (API):\n")
127 f.write("```bash\n")
128 f.write(f"./llama-server -m {Path(output_path).name}\n")
129 f.write("```\n\n")
130 f.write("Затем отправьте запрос:\n")
131 f.write("```json\n")
132 f.write('{"messages": [{"role": "system", "content": "' + new_system_prompt.replace('"', '\\"').split('\n')[0] + '..."}], }\n')
133 f.write("```\n")
134
135 return info_file
136
137
138 def main():
139 if len(sys.argv) < 4:
140 print("Использование:")
141 print(f" {sys.argv[0]} <input.gguf> <output.gguf> <prompt_file.txt>")
142 print("\nПример:")
143 print(f" {sys.argv[0]} model.Q4_K_M.gguf model_with_prompt.Q4_K_M.gguf prompt.txt")
144 sys.exit(1)
145
146 input_gguf = Path(sys.argv[1])
147 output_gguf = Path(sys.argv[2])
148 prompt_file = Path(sys.argv[3])
149
150 # Проверяем файлы
151 if not input_gguf.exists():
152 print(f"❌ Не найден: {input_gguf}")
153 sys.exit(1)
154
155 if not prompt_file.exists():
156 print(f"❌ Не найден: {prompt_file}")
157 sys.exit(1)
158
159 # Читаем промпт
160 print(f"\n📄 Читаю промпт из: {prompt_file}")
161 with open(prompt_file, "r", encoding="utf-8") as f:
162 system_prompt = f.read().strip()
163
164 print(f"✅ Промпт ({len(system_prompt)} символов):")
165 print("-" * 60)
166 print(system_prompt[:300])
167 if len(system_prompt) > 300:
168 print("...")
169 print("-" * 60)
170
171 # Обновляем GGUF
172 info_file = create_gguf_with_new_metadata(input_gguf, output_gguf, system_prompt)
173
174 file_size_gb = output_gguf.stat().st_size / (1024**3)
175 print(f"\n✅ ГОТОВО!")
176 print(f"📦 Выходной GGUF: {output_gguf}")
177 print(f"📋 Размер: {file_size_gb:.2f} GB")
178 print(f"📝 Информация о промпте: {info_file}")
179 print(f"\nРазошли людям файл: {output_gguf.name}")
180
181
182 if __name__ == "__main__":
183 main()
184