embed_prompt_gguf_proper.py
| 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 | |