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.
Files changed (55) hide show
  1. {pysfi-0.1.7.dist-info → pysfi-0.1.11.dist-info}/METADATA +11 -9
  2. pysfi-0.1.11.dist-info/RECORD +60 -0
  3. pysfi-0.1.11.dist-info/entry_points.txt +28 -0
  4. sfi/__init__.py +1 -1
  5. sfi/alarmclock/alarmclock.py +40 -40
  6. sfi/bumpversion/__init__.py +1 -1
  7. sfi/cleanbuild/cleanbuild.py +155 -0
  8. sfi/condasetup/condasetup.py +116 -0
  9. sfi/docscan/__init__.py +1 -1
  10. sfi/docscan/docscan.py +407 -103
  11. sfi/docscan/docscan_gui.py +1282 -596
  12. sfi/docscan/lang/eng.py +152 -0
  13. sfi/docscan/lang/zhcn.py +170 -0
  14. sfi/filedate/filedate.py +185 -112
  15. sfi/gittool/__init__.py +2 -0
  16. sfi/gittool/gittool.py +401 -0
  17. sfi/llmclient/llmclient.py +592 -0
  18. sfi/llmquantize/llmquantize.py +480 -0
  19. sfi/llmserver/llmserver.py +335 -0
  20. sfi/makepython/makepython.py +31 -30
  21. sfi/pdfsplit/pdfsplit.py +173 -173
  22. sfi/pyarchive/pyarchive.py +418 -0
  23. sfi/pyembedinstall/pyembedinstall.py +629 -0
  24. sfi/pylibpack/__init__.py +0 -0
  25. sfi/pylibpack/pylibpack.py +1457 -0
  26. sfi/pylibpack/rules/numpy.json +22 -0
  27. sfi/pylibpack/rules/pymupdf.json +10 -0
  28. sfi/pylibpack/rules/pyqt5.json +19 -0
  29. sfi/pylibpack/rules/pyside2.json +23 -0
  30. sfi/pylibpack/rules/scipy.json +23 -0
  31. sfi/pylibpack/rules/shiboken2.json +24 -0
  32. sfi/pyloadergen/pyloadergen.py +512 -227
  33. sfi/pypack/__init__.py +0 -0
  34. sfi/pypack/pypack.py +1142 -0
  35. sfi/pyprojectparse/__init__.py +0 -0
  36. sfi/pyprojectparse/pyprojectparse.py +500 -0
  37. sfi/pysourcepack/pysourcepack.py +308 -0
  38. sfi/quizbase/__init__.py +0 -0
  39. sfi/quizbase/quizbase.py +828 -0
  40. sfi/quizbase/quizbase_gui.py +987 -0
  41. sfi/regexvalidate/__init__.py +0 -0
  42. sfi/regexvalidate/regex_help.html +284 -0
  43. sfi/regexvalidate/regexvalidate.py +468 -0
  44. sfi/taskkill/taskkill.py +0 -2
  45. sfi/workflowengine/__init__.py +0 -0
  46. sfi/workflowengine/workflowengine.py +444 -0
  47. pysfi-0.1.7.dist-info/RECORD +0 -31
  48. pysfi-0.1.7.dist-info/entry_points.txt +0 -15
  49. sfi/embedinstall/embedinstall.py +0 -418
  50. sfi/projectparse/projectparse.py +0 -152
  51. sfi/pypacker/fspacker.py +0 -91
  52. {pysfi-0.1.7.dist-info → pysfi-0.1.11.dist-info}/WHEEL +0 -0
  53. /sfi/{embedinstall → docscan/lang}/__init__.py +0 -0
  54. /sfi/{projectparse → llmquantize}/__init__.py +0 -0
  55. /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_())