pysfi 0.1.7__py3-none-any.whl → 0.1.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pysfi-0.1.7.dist-info → pysfi-0.1.11.dist-info}/METADATA +11 -9
- pysfi-0.1.11.dist-info/RECORD +60 -0
- pysfi-0.1.11.dist-info/entry_points.txt +28 -0
- sfi/__init__.py +1 -1
- sfi/alarmclock/alarmclock.py +40 -40
- sfi/bumpversion/__init__.py +1 -1
- sfi/cleanbuild/cleanbuild.py +155 -0
- sfi/condasetup/condasetup.py +116 -0
- sfi/docscan/__init__.py +1 -1
- sfi/docscan/docscan.py +407 -103
- sfi/docscan/docscan_gui.py +1282 -596
- sfi/docscan/lang/eng.py +152 -0
- sfi/docscan/lang/zhcn.py +170 -0
- sfi/filedate/filedate.py +185 -112
- sfi/gittool/__init__.py +2 -0
- sfi/gittool/gittool.py +401 -0
- sfi/llmclient/llmclient.py +592 -0
- sfi/llmquantize/llmquantize.py +480 -0
- sfi/llmserver/llmserver.py +335 -0
- sfi/makepython/makepython.py +31 -30
- sfi/pdfsplit/pdfsplit.py +173 -173
- sfi/pyarchive/pyarchive.py +418 -0
- sfi/pyembedinstall/pyembedinstall.py +629 -0
- sfi/pylibpack/__init__.py +0 -0
- sfi/pylibpack/pylibpack.py +1457 -0
- sfi/pylibpack/rules/numpy.json +22 -0
- sfi/pylibpack/rules/pymupdf.json +10 -0
- sfi/pylibpack/rules/pyqt5.json +19 -0
- sfi/pylibpack/rules/pyside2.json +23 -0
- sfi/pylibpack/rules/scipy.json +23 -0
- sfi/pylibpack/rules/shiboken2.json +24 -0
- sfi/pyloadergen/pyloadergen.py +512 -227
- sfi/pypack/__init__.py +0 -0
- sfi/pypack/pypack.py +1142 -0
- sfi/pyprojectparse/__init__.py +0 -0
- sfi/pyprojectparse/pyprojectparse.py +500 -0
- sfi/pysourcepack/pysourcepack.py +308 -0
- sfi/quizbase/__init__.py +0 -0
- sfi/quizbase/quizbase.py +828 -0
- sfi/quizbase/quizbase_gui.py +987 -0
- sfi/regexvalidate/__init__.py +0 -0
- sfi/regexvalidate/regex_help.html +284 -0
- sfi/regexvalidate/regexvalidate.py +468 -0
- sfi/taskkill/taskkill.py +0 -2
- sfi/workflowengine/__init__.py +0 -0
- sfi/workflowengine/workflowengine.py +444 -0
- pysfi-0.1.7.dist-info/RECORD +0 -31
- pysfi-0.1.7.dist-info/entry_points.txt +0 -15
- sfi/embedinstall/embedinstall.py +0 -418
- sfi/projectparse/projectparse.py +0 -152
- sfi/pypacker/fspacker.py +0 -91
- {pysfi-0.1.7.dist-info → pysfi-0.1.11.dist-info}/WHEEL +0 -0
- /sfi/{embedinstall → docscan/lang}/__init__.py +0 -0
- /sfi/{projectparse → llmquantize}/__init__.py +0 -0
- /sfi/{pypacker → pyembedinstall}/__init__.py +0 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"""GGUF量化转换GUI工具, 用于将F16格式的GGUF文件转换为其他主流量化格式."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import pathlib
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from types import SimpleNamespace
|
|
13
|
+
|
|
14
|
+
from PySide2.QtCore import QThread, Signal, Slot
|
|
15
|
+
from PySide2.QtGui import QMoveEvent, QResizeEvent
|
|
16
|
+
from PySide2.QtWidgets import (
|
|
17
|
+
QApplication,
|
|
18
|
+
QCheckBox,
|
|
19
|
+
QFileDialog,
|
|
20
|
+
QGridLayout,
|
|
21
|
+
QGroupBox,
|
|
22
|
+
QHBoxLayout,
|
|
23
|
+
QLabel,
|
|
24
|
+
QMainWindow,
|
|
25
|
+
QProgressBar,
|
|
26
|
+
QPushButton,
|
|
27
|
+
QTextEdit,
|
|
28
|
+
QVBoxLayout,
|
|
29
|
+
QWidget,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
CONFIG_FILE = Path.home() / ".sfi" / "llmquantize.json"
|
|
33
|
+
|
|
34
|
+
logging.basicConfig(level=logging.INFO)
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class QuantizerConfig(SimpleNamespace):
|
|
39
|
+
"""GGUF量化转换工具配置."""
|
|
40
|
+
|
|
41
|
+
TITLE: str = "GGUF量化转换工具"
|
|
42
|
+
WIN_SIZE: list[int] = [600, 500] # noqa: RUF012
|
|
43
|
+
WIN_POS: list[int] = [100, 100] # noqa: RUF012
|
|
44
|
+
LAST_INPUT_FILE: str = ""
|
|
45
|
+
SELECTED_QUANTS: list[str] = ["Q4_K_M", "Q5_K_M"] # noqa: RUF012
|
|
46
|
+
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
if CONFIG_FILE.exists():
|
|
49
|
+
logger.info("Loading configuration from %s", CONFIG_FILE)
|
|
50
|
+
try:
|
|
51
|
+
# 直接更新,忽略无效字段
|
|
52
|
+
self.__dict__.update(json.loads(CONFIG_FILE.read_text()))
|
|
53
|
+
except (json.JSONDecodeError, TypeError) as e:
|
|
54
|
+
logger.warning("Failed to load configuration: %s", e)
|
|
55
|
+
logger.info("Using default configuration")
|
|
56
|
+
else:
|
|
57
|
+
logger.info("Using default configuration")
|
|
58
|
+
|
|
59
|
+
def save(self) -> None:
|
|
60
|
+
"""保存配置."""
|
|
61
|
+
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
CONFIG_FILE.write_text(json.dumps(vars(self), indent=4))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
conf = QuantizerConfig()
|
|
66
|
+
atexit.register(conf.save)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _process_gguf_stem(filename: str) -> str:
|
|
70
|
+
"""处理文件名, 移除可能的F16后缀.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
str: 处理后的文件名
|
|
74
|
+
"""
|
|
75
|
+
if filename.upper().endswith("-F16"):
|
|
76
|
+
filename = filename[:-4] # 移除-F16后缀
|
|
77
|
+
return filename
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class QuantizationWorker(QThread):
|
|
81
|
+
"""量化执行线程Worker."""
|
|
82
|
+
|
|
83
|
+
progress_msg_updated = Signal(str)
|
|
84
|
+
progress_count_updated = Signal(int)
|
|
85
|
+
is_finished = Signal()
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
input_file: pathlib.Path,
|
|
90
|
+
quant_types: list[str],
|
|
91
|
+
) -> None:
|
|
92
|
+
super().__init__()
|
|
93
|
+
|
|
94
|
+
self.input_file = input_file
|
|
95
|
+
self.quant_types = quant_types
|
|
96
|
+
self.input_dir = input_file.parent
|
|
97
|
+
self.base_name = _process_gguf_stem(input_file.stem)
|
|
98
|
+
self.total_files = len(quant_types)
|
|
99
|
+
self.completed_files = 0
|
|
100
|
+
self.success = True # 记录整体转换状态
|
|
101
|
+
|
|
102
|
+
def run(self) -> None:
|
|
103
|
+
"""执行量化转换任务."""
|
|
104
|
+
try:
|
|
105
|
+
for quant_type in self.quant_types:
|
|
106
|
+
output_file: pathlib.Path = (
|
|
107
|
+
self.input_dir / f"{self.base_name}-{quant_type}.gguf"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
self.progress_msg_updated.emit(
|
|
111
|
+
f"正在转换到 {quant_type} 格式...",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# 构建命令行参数(确保所有参数都是字符串)
|
|
115
|
+
cmd = [
|
|
116
|
+
"llama-quantize",
|
|
117
|
+
str(self.input_file.name),
|
|
118
|
+
str(output_file.name),
|
|
119
|
+
quant_type,
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
# 执行转换命令(使用 cwd 参数避免全局目录变更)
|
|
123
|
+
try:
|
|
124
|
+
process = subprocess.Popen(
|
|
125
|
+
cmd,
|
|
126
|
+
stdout=subprocess.PIPE,
|
|
127
|
+
stderr=subprocess.STDOUT,
|
|
128
|
+
universal_newlines=True,
|
|
129
|
+
bufsize=1, # 行缓冲以获得实时输出
|
|
130
|
+
cwd=str(self.input_file.parent),
|
|
131
|
+
)
|
|
132
|
+
except (FileNotFoundError, PermissionError) as e:
|
|
133
|
+
self.progress_msg_updated.emit(f"无法启动量化进程: {e!s}")
|
|
134
|
+
self.success = False
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
# 实时输出进度
|
|
138
|
+
if not process.stdout:
|
|
139
|
+
logger.error("无法获取进度信息")
|
|
140
|
+
process.wait()
|
|
141
|
+
self.success = False
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
for line in process.stdout:
|
|
146
|
+
self.progress_msg_updated.emit(line.strip())
|
|
147
|
+
finally:
|
|
148
|
+
# 等待进程结束,设置超时防止永久挂起
|
|
149
|
+
try:
|
|
150
|
+
process.wait(timeout=3600) # 单个文件最多1小时
|
|
151
|
+
except subprocess.TimeoutExpired:
|
|
152
|
+
self.progress_msg_updated.emit(f"转换 {quant_type} 超时")
|
|
153
|
+
process.kill()
|
|
154
|
+
process.wait()
|
|
155
|
+
self.success = False
|
|
156
|
+
continue
|
|
157
|
+
finally:
|
|
158
|
+
# 确保关闭管道以避免资源泄漏
|
|
159
|
+
if process.stdout:
|
|
160
|
+
process.stdout.close()
|
|
161
|
+
|
|
162
|
+
# 只有在成功时才更新进度
|
|
163
|
+
if process.returncode == 0:
|
|
164
|
+
self.completed_files += 1
|
|
165
|
+
progress = int((self.completed_files / self.total_files) * 100)
|
|
166
|
+
self.progress_count_updated.emit(progress)
|
|
167
|
+
self.progress_msg_updated.emit(
|
|
168
|
+
f"成功生成: {output_file!s}",
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
self.progress_msg_updated.emit(f"转换 {quant_type} 失败")
|
|
172
|
+
self.success = False
|
|
173
|
+
|
|
174
|
+
self.is_finished.emit()
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.exception("量化转换过程中发生异常")
|
|
177
|
+
self.progress_msg_updated.emit(f"发生错误: {e!s}")
|
|
178
|
+
self.success = False
|
|
179
|
+
self.is_finished.emit()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class GGUFQuantizerGUI(QMainWindow):
|
|
183
|
+
"""GGUF量化转换工具GUI界面."""
|
|
184
|
+
|
|
185
|
+
def __init__(self) -> None:
|
|
186
|
+
super().__init__()
|
|
187
|
+
self.setWindowTitle(conf.TITLE)
|
|
188
|
+
self.setGeometry(*conf.WIN_POS, *conf.WIN_SIZE)
|
|
189
|
+
|
|
190
|
+
self.input_file: pathlib.Path = pathlib.Path(conf.LAST_INPUT_FILE)
|
|
191
|
+
self.worker: QuantizationWorker | None = None
|
|
192
|
+
self.quant_types = {
|
|
193
|
+
"Q2_K": "Q2_K (极低精度, 最小尺寸)",
|
|
194
|
+
"Q3_K_S": "Q3_K_S (低精度, 小尺寸)",
|
|
195
|
+
"Q3_K_M": "Q3_K_M (低精度, 中等尺寸)",
|
|
196
|
+
"Q3_K_L": "Q3_K_L (低精度, 大尺寸)",
|
|
197
|
+
"Q4_0": "Q4_0 (基本4位)",
|
|
198
|
+
"Q4_K_S": "Q4_K_S (4位, 小尺寸)",
|
|
199
|
+
"Q4_K_M": "Q4_K_M (4位, 中等尺寸)",
|
|
200
|
+
"Q5_0": "Q5_0 (基本5位)",
|
|
201
|
+
"Q5_K_S": "Q5_K_S (5位, 小尺寸)",
|
|
202
|
+
"Q5_K_M": "Q5_K_M (5位, 中等尺寸)",
|
|
203
|
+
"Q6_K": "Q6_K (6位, 高质量)",
|
|
204
|
+
"Q8_0": "Q8_0 (8位, 最高质量)",
|
|
205
|
+
}
|
|
206
|
+
self.quant_checks = {} # 存储量化类型对应的checkbox
|
|
207
|
+
|
|
208
|
+
self.init_ui()
|
|
209
|
+
|
|
210
|
+
# 恢复上次选择的文件
|
|
211
|
+
if conf.LAST_INPUT_FILE and pathlib.Path(conf.LAST_INPUT_FILE).exists():
|
|
212
|
+
self.input_file = pathlib.Path(conf.LAST_INPUT_FILE)
|
|
213
|
+
self.file_label.setText(self.input_file.name)
|
|
214
|
+
self.check_existing_quant_files()
|
|
215
|
+
self.convert_btn.setEnabled(True)
|
|
216
|
+
|
|
217
|
+
def init_ui(self) -> None:
|
|
218
|
+
"""初始化界面."""
|
|
219
|
+
main_widget = QWidget()
|
|
220
|
+
main_layout = QVBoxLayout()
|
|
221
|
+
|
|
222
|
+
# 文件选择部分
|
|
223
|
+
file_group = QGroupBox("选择F16格式的GGUF文件")
|
|
224
|
+
file_layout = QVBoxLayout()
|
|
225
|
+
|
|
226
|
+
self.file_label = QLabel("未选择文件")
|
|
227
|
+
file_btn = QPushButton("选择文件")
|
|
228
|
+
file_btn.clicked.connect(self.select_file)
|
|
229
|
+
|
|
230
|
+
file_layout.addWidget(self.file_label)
|
|
231
|
+
file_layout.addWidget(file_btn)
|
|
232
|
+
file_group.setLayout(file_layout)
|
|
233
|
+
|
|
234
|
+
# 量化选项部分
|
|
235
|
+
quant_group = QGroupBox("选择量化类型")
|
|
236
|
+
quant_layout = QGridLayout()
|
|
237
|
+
|
|
238
|
+
for i, (quant_type, label) in enumerate(self.quant_types.items()):
|
|
239
|
+
check = QCheckBox(label)
|
|
240
|
+
# 连接信号,选择时保存配置
|
|
241
|
+
check.stateChanged.connect(self.on_quant_type_changed)
|
|
242
|
+
self.quant_checks[quant_type] = check
|
|
243
|
+
row = i // 2
|
|
244
|
+
col = i % 2
|
|
245
|
+
quant_layout.addWidget(check, row, col)
|
|
246
|
+
|
|
247
|
+
# 恢复上次选择的量化类型
|
|
248
|
+
for quant_type in conf.SELECTED_QUANTS:
|
|
249
|
+
if quant_type in self.quant_checks:
|
|
250
|
+
self.quant_checks[quant_type].setChecked(True)
|
|
251
|
+
|
|
252
|
+
quant_group.setLayout(quant_layout)
|
|
253
|
+
|
|
254
|
+
# 进度显示部分
|
|
255
|
+
progress_group = QGroupBox("转换进度")
|
|
256
|
+
progress_layout = QVBoxLayout()
|
|
257
|
+
|
|
258
|
+
self.progress_bar = QProgressBar()
|
|
259
|
+
self.progress_bar.setRange(0, 100)
|
|
260
|
+
self.progress_bar.setValue(0)
|
|
261
|
+
|
|
262
|
+
self.output_text = QTextEdit()
|
|
263
|
+
self.output_text.setReadOnly(True)
|
|
264
|
+
|
|
265
|
+
progress_layout.addWidget(self.progress_bar)
|
|
266
|
+
progress_layout.addWidget(self.output_text)
|
|
267
|
+
progress_group.setLayout(progress_layout)
|
|
268
|
+
|
|
269
|
+
# 操作按钮
|
|
270
|
+
btn_layout = QHBoxLayout()
|
|
271
|
+
self.convert_btn = QPushButton("开始转换")
|
|
272
|
+
self.convert_btn.clicked.connect(self.start_conversion)
|
|
273
|
+
self.convert_btn.setEnabled(False)
|
|
274
|
+
|
|
275
|
+
btn_layout.addWidget(self.convert_btn)
|
|
276
|
+
|
|
277
|
+
# 组装主界面
|
|
278
|
+
main_layout.addWidget(file_group)
|
|
279
|
+
main_layout.addWidget(quant_group)
|
|
280
|
+
main_layout.addWidget(progress_group)
|
|
281
|
+
main_layout.addLayout(btn_layout)
|
|
282
|
+
|
|
283
|
+
main_widget.setLayout(main_layout)
|
|
284
|
+
self.setCentralWidget(main_widget)
|
|
285
|
+
|
|
286
|
+
def select_file(self) -> None:
|
|
287
|
+
"""选择文件."""
|
|
288
|
+
# 使用上次选择的目录作为初始目录
|
|
289
|
+
initial_dir = ""
|
|
290
|
+
if conf.LAST_INPUT_FILE and pathlib.Path(conf.LAST_INPUT_FILE).exists():
|
|
291
|
+
initial_dir = str(pathlib.Path(conf.LAST_INPUT_FILE).parent)
|
|
292
|
+
elif self.input_file.exists():
|
|
293
|
+
initial_dir = str(self.input_file.parent)
|
|
294
|
+
|
|
295
|
+
file_path, _ = QFileDialog.getOpenFileName(
|
|
296
|
+
self,
|
|
297
|
+
"选择F16格式的GGUF文件",
|
|
298
|
+
initial_dir,
|
|
299
|
+
"GGUF Files (*.gguf)",
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
if file_path:
|
|
303
|
+
self.input_file = pathlib.Path(file_path)
|
|
304
|
+
filename = self.input_file.name
|
|
305
|
+
self.file_label.setText(filename)
|
|
306
|
+
|
|
307
|
+
# 保存最后选择的文件路径
|
|
308
|
+
conf.LAST_INPUT_FILE = file_path
|
|
309
|
+
|
|
310
|
+
# 检查文件名是否包含F16
|
|
311
|
+
if "-F16" not in filename.upper():
|
|
312
|
+
self.output_text.append(
|
|
313
|
+
"注意: 输入文件名不包含F16后缀,输出文件名将直接添加量化类型",
|
|
314
|
+
)
|
|
315
|
+
self._scroll_to_bottom()
|
|
316
|
+
|
|
317
|
+
# 检查已存在的量化文件
|
|
318
|
+
self.check_existing_quant_files()
|
|
319
|
+
|
|
320
|
+
self.convert_btn.setEnabled(True)
|
|
321
|
+
self.output_text.clear()
|
|
322
|
+
self.progress_bar.setValue(0)
|
|
323
|
+
self._scroll_to_bottom()
|
|
324
|
+
|
|
325
|
+
def check_existing_quant_files(self) -> None:
|
|
326
|
+
"""检查当前目录下已存在的量化文件, 并更新checkbox状态."""
|
|
327
|
+
if not self.input_file:
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
dir_path = self.input_file.parent
|
|
331
|
+
|
|
332
|
+
for quant_type in self.quant_types:
|
|
333
|
+
filename = f"{_process_gguf_stem(self.input_file.stem)}-{quant_type}.gguf"
|
|
334
|
+
expected_file = dir_path / filename
|
|
335
|
+
if expected_file.exists():
|
|
336
|
+
# 文件已存在,标记但不禁用,允许用户选择重新生成
|
|
337
|
+
self.quant_checks[quant_type].setText(
|
|
338
|
+
f"{self.quant_types[quant_type]} (已存在)",
|
|
339
|
+
)
|
|
340
|
+
self.quant_checks[quant_type].setStyleSheet("color: orange;")
|
|
341
|
+
else:
|
|
342
|
+
self.quant_checks[quant_type].setText(
|
|
343
|
+
self.quant_types[quant_type],
|
|
344
|
+
)
|
|
345
|
+
self.quant_checks[quant_type].setStyleSheet("")
|
|
346
|
+
|
|
347
|
+
def _scroll_to_bottom(self) -> None:
|
|
348
|
+
"""滚动输出框到底部."""
|
|
349
|
+
scrollbar = self.output_text.verticalScrollBar()
|
|
350
|
+
scrollbar.setValue(scrollbar.maximum())
|
|
351
|
+
|
|
352
|
+
def on_quant_type_changed(self, _state: int) -> None:
|
|
353
|
+
"""量化类型变更时保存配置."""
|
|
354
|
+
selected_quants = [
|
|
355
|
+
q for q, check in self.quant_checks.items() if check.isChecked()
|
|
356
|
+
]
|
|
357
|
+
conf.SELECTED_QUANTS = selected_quants
|
|
358
|
+
|
|
359
|
+
def moveEvent(self, event: QMoveEvent) -> None: # noqa: N802
|
|
360
|
+
"""处理窗口移动事件,保存窗口位置."""
|
|
361
|
+
top_left = self.geometry().topLeft()
|
|
362
|
+
conf.WIN_POS = [top_left.x(), top_left.y()]
|
|
363
|
+
return super().moveEvent(event)
|
|
364
|
+
|
|
365
|
+
def resizeEvent(self, event: QResizeEvent) -> None: # noqa: N802
|
|
366
|
+
"""处理窗口调整大小事件,保存窗口大小."""
|
|
367
|
+
geometry = self.geometry()
|
|
368
|
+
conf.WIN_SIZE = [geometry.width(), geometry.height()]
|
|
369
|
+
return super().resizeEvent(event)
|
|
370
|
+
|
|
371
|
+
def start_conversion(self) -> None:
|
|
372
|
+
"""开始转换."""
|
|
373
|
+
# 检查是否已有任务在运行
|
|
374
|
+
if self.worker and self.worker.isRunning():
|
|
375
|
+
self.output_text.append("已有转换任务正在进行, 请等待完成")
|
|
376
|
+
self._scroll_to_bottom()
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
selected_quants: list[str] = [
|
|
380
|
+
q for q, check in self.quant_checks.items() if check.isChecked()
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
if not selected_quants:
|
|
384
|
+
self.output_text.append("请至少选择一种量化类型")
|
|
385
|
+
self._scroll_to_bottom()
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
if not self.input_file:
|
|
389
|
+
self.output_text.append("请先选择输入文件")
|
|
390
|
+
self._scroll_to_bottom()
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
# 检查是否有已存在的文件将被覆盖
|
|
394
|
+
existing_files = []
|
|
395
|
+
for quant_type in selected_quants:
|
|
396
|
+
filename = f"{_process_gguf_stem(self.input_file.stem)}-{quant_type}.gguf"
|
|
397
|
+
expected_file = self.input_file.parent / filename
|
|
398
|
+
if expected_file.exists():
|
|
399
|
+
existing_files.append(filename)
|
|
400
|
+
|
|
401
|
+
if existing_files:
|
|
402
|
+
self.output_text.append("警告: 将覆盖以下已存在文件:")
|
|
403
|
+
for existing_file in existing_files:
|
|
404
|
+
self.output_text.append(f" - {existing_file}")
|
|
405
|
+
self._scroll_to_bottom()
|
|
406
|
+
|
|
407
|
+
self.convert_btn.setEnabled(False)
|
|
408
|
+
self.progress_bar.setValue(0)
|
|
409
|
+
self.output_text.append(f"开始转换: {self.input_file!s}")
|
|
410
|
+
self.output_text.append(f"选择的量化类型: {', '.join(selected_quants)}")
|
|
411
|
+
self.output_text.append(f"将生成 {len(selected_quants)} 个量化文件")
|
|
412
|
+
|
|
413
|
+
self.worker = QuantizationWorker(self.input_file, selected_quants)
|
|
414
|
+
# 连接信号
|
|
415
|
+
self.worker.progress_msg_updated.connect(self.update_progress_msg)
|
|
416
|
+
self.worker.is_finished.connect(self.conversion_finished)
|
|
417
|
+
self.worker.progress_count_updated.connect(self.update_progress_value)
|
|
418
|
+
self.worker.start()
|
|
419
|
+
|
|
420
|
+
@Slot(str)
|
|
421
|
+
def update_progress_msg(self, message: str) -> None:
|
|
422
|
+
"""更新进度信息."""
|
|
423
|
+
self.output_text.append(message)
|
|
424
|
+
self.output_text.ensureCursorVisible()
|
|
425
|
+
self._scroll_to_bottom()
|
|
426
|
+
|
|
427
|
+
@Slot(int)
|
|
428
|
+
def update_progress_value(self, value: int) -> None:
|
|
429
|
+
"""更新进度条."""
|
|
430
|
+
self.progress_bar.setValue(value)
|
|
431
|
+
|
|
432
|
+
@Slot()
|
|
433
|
+
def conversion_finished(self) -> None:
|
|
434
|
+
"""转换完成回调函数."""
|
|
435
|
+
success = self.worker.success if self.worker else False
|
|
436
|
+
|
|
437
|
+
# 断开信号连接,允许 worker 被垃圾回收
|
|
438
|
+
if self.worker:
|
|
439
|
+
try:
|
|
440
|
+
self.worker.progress_msg_updated.disconnect(self.update_progress_msg)
|
|
441
|
+
self.worker.is_finished.disconnect(self.conversion_finished)
|
|
442
|
+
self.worker.progress_count_updated.disconnect(
|
|
443
|
+
self.update_progress_value
|
|
444
|
+
)
|
|
445
|
+
except RuntimeError:
|
|
446
|
+
# Worker 可能已经被销毁
|
|
447
|
+
pass
|
|
448
|
+
|
|
449
|
+
self.convert_btn.setEnabled(True)
|
|
450
|
+
|
|
451
|
+
if success:
|
|
452
|
+
self.output_text.append("所有量化转换完成!")
|
|
453
|
+
self.progress_bar.setValue(100)
|
|
454
|
+
# 重新检查已存在的量化文件
|
|
455
|
+
self.check_existing_quant_files()
|
|
456
|
+
else:
|
|
457
|
+
self.output_text.append("量化转换过程中出现错误!")
|
|
458
|
+
self._scroll_to_bottom()
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def main() -> None:
|
|
462
|
+
app = QApplication(sys.argv)
|
|
463
|
+
|
|
464
|
+
# 检查是否安装了llama.cpp
|
|
465
|
+
try:
|
|
466
|
+
subprocess.run(
|
|
467
|
+
["llama-quantize", "--help"],
|
|
468
|
+
capture_output=True,
|
|
469
|
+
check=False,
|
|
470
|
+
)
|
|
471
|
+
except FileNotFoundError:
|
|
472
|
+
logger.exception("错误: 未找到llama.cpp/quantize工具")
|
|
473
|
+
logger.exception(
|
|
474
|
+
"请确保已编译llama.cpp并将quantize工具放在llama.cpp/目录下",
|
|
475
|
+
)
|
|
476
|
+
sys.exit(1)
|
|
477
|
+
|
|
478
|
+
window = GGUFQuantizerGUI()
|
|
479
|
+
window.show()
|
|
480
|
+
sys.exit(app.exec_())
|