pysfi 0.1.12__py3-none-any.whl → 0.1.14__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.12.dist-info → pysfi-0.1.14.dist-info}/METADATA +1 -1
- pysfi-0.1.14.dist-info/RECORD +68 -0
- {pysfi-0.1.12.dist-info → pysfi-0.1.14.dist-info}/entry_points.txt +3 -0
- sfi/__init__.py +19 -2
- sfi/alarmclock/__init__.py +3 -0
- sfi/alarmclock/alarmclock.py +23 -40
- sfi/bumpversion/__init__.py +3 -1
- sfi/bumpversion/bumpversion.py +64 -15
- sfi/cleanbuild/__init__.py +3 -0
- sfi/cleanbuild/cleanbuild.py +5 -1
- sfi/cli.py +25 -4
- sfi/condasetup/__init__.py +1 -0
- sfi/condasetup/condasetup.py +91 -76
- sfi/docdiff/__init__.py +1 -0
- sfi/docdiff/docdiff.py +3 -2
- sfi/docscan/__init__.py +1 -1
- sfi/docscan/docscan.py +78 -23
- sfi/docscan/docscan_gui.py +152 -48
- sfi/filedate/filedate.py +12 -5
- sfi/img2pdf/img2pdf.py +453 -0
- sfi/llmclient/llmclient.py +31 -8
- sfi/llmquantize/llmquantize.py +76 -37
- sfi/llmserver/__init__.py +1 -0
- sfi/llmserver/llmserver.py +63 -13
- sfi/makepython/makepython.py +1145 -201
- sfi/pdfsplit/pdfsplit.py +45 -12
- sfi/pyarchive/__init__.py +1 -0
- sfi/pyarchive/pyarchive.py +908 -278
- sfi/pyembedinstall/pyembedinstall.py +88 -89
- sfi/pylibpack/pylibpack.py +561 -463
- sfi/pyloadergen/pyloadergen.py +372 -218
- sfi/pypack/pypack.py +510 -959
- sfi/pyprojectparse/pyprojectparse.py +337 -40
- sfi/pysourcepack/__init__.py +1 -0
- sfi/pysourcepack/pysourcepack.py +210 -131
- sfi/quizbase/quizbase_gui.py +2 -2
- sfi/taskkill/taskkill.py +168 -59
- sfi/which/which.py +11 -3
- pysfi-0.1.12.dist-info/RECORD +0 -62
- sfi/workflowengine/workflowengine.py +0 -444
- {pysfi-0.1.12.dist-info → pysfi-0.1.14.dist-info}/WHEEL +0 -0
- /sfi/{workflowengine → img2pdf}/__init__.py +0 -0
sfi/llmquantize/llmquantize.py
CHANGED
|
@@ -8,8 +8,8 @@ import logging
|
|
|
8
8
|
import pathlib
|
|
9
9
|
import subprocess
|
|
10
10
|
import sys
|
|
11
|
+
from dataclasses import dataclass
|
|
11
12
|
from pathlib import Path
|
|
12
|
-
from types import SimpleNamespace
|
|
13
13
|
|
|
14
14
|
from PySide2.QtCore import QThread, Signal, Slot
|
|
15
15
|
from PySide2.QtGui import QMoveEvent, QResizeEvent
|
|
@@ -29,37 +29,65 @@ from PySide2.QtWidgets import (
|
|
|
29
29
|
QWidget,
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
-
CONFIG_FILE = Path.home() / ".
|
|
32
|
+
CONFIG_FILE = Path.home() / ".pysfi" / "llmquantize.json"
|
|
33
33
|
|
|
34
|
-
logging.basicConfig(
|
|
34
|
+
logging.basicConfig(
|
|
35
|
+
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
|
36
|
+
)
|
|
35
37
|
logger = logging.getLogger(__name__)
|
|
36
38
|
|
|
39
|
+
__version__ = "1.0.0"
|
|
40
|
+
__build__ = "20260204"
|
|
41
|
+
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
@dataclass
|
|
44
|
+
class QuantizerConfig:
|
|
39
45
|
"""GGUF量化转换工具配置."""
|
|
40
46
|
|
|
41
47
|
TITLE: str = "GGUF量化转换工具"
|
|
42
|
-
WIN_SIZE: list[int] =
|
|
43
|
-
WIN_POS: list[int] =
|
|
48
|
+
WIN_SIZE: list[int] = None
|
|
49
|
+
WIN_POS: list[int] = None
|
|
44
50
|
LAST_INPUT_FILE: str = ""
|
|
45
|
-
SELECTED_QUANTS: list[str] =
|
|
51
|
+
SELECTED_QUANTS: list[str] = None
|
|
52
|
+
|
|
53
|
+
def __post_init__(self) -> None:
|
|
54
|
+
"""初始化默认值并加载配置文件."""
|
|
55
|
+
# 初始化默认值
|
|
56
|
+
if self.WIN_SIZE is None:
|
|
57
|
+
self.WIN_SIZE = [600, 500]
|
|
58
|
+
if self.WIN_POS is None:
|
|
59
|
+
self.WIN_POS = [100, 100]
|
|
60
|
+
if self.SELECTED_QUANTS is None:
|
|
61
|
+
self.SELECTED_QUANTS = ["Q4_K_M", "Q5_K_M"]
|
|
46
62
|
|
|
47
|
-
def __init__(self) -> None:
|
|
48
63
|
if CONFIG_FILE.exists():
|
|
49
64
|
logger.info("Loading configuration from %s", CONFIG_FILE)
|
|
50
65
|
try:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
config_data = json.loads(CONFIG_FILE.read_text())
|
|
67
|
+
# 更新实例属性,只更新存在的属性
|
|
68
|
+
for key, value in config_data.items():
|
|
69
|
+
if hasattr(self, key):
|
|
70
|
+
setattr(self, key, value)
|
|
71
|
+
except (json.JSONDecodeError, TypeError, AttributeError) as e:
|
|
54
72
|
logger.warning("Failed to load configuration: %s", e)
|
|
55
73
|
logger.info("Using default configuration")
|
|
56
74
|
else:
|
|
57
75
|
logger.info("Using default configuration")
|
|
58
76
|
|
|
59
77
|
def save(self) -> None:
|
|
60
|
-
"""
|
|
78
|
+
"""保存配置到文件."""
|
|
61
79
|
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
-
|
|
80
|
+
# 将数据类转换为字典进行JSON序列化
|
|
81
|
+
config_dict = {}
|
|
82
|
+
for attr_name in dir(self):
|
|
83
|
+
if not attr_name.startswith("_") and attr_name not in ["TITLE"]:
|
|
84
|
+
try:
|
|
85
|
+
attr_value = getattr(self, attr_name)
|
|
86
|
+
if not callable(attr_value):
|
|
87
|
+
config_dict[attr_name] = attr_value
|
|
88
|
+
except AttributeError:
|
|
89
|
+
continue
|
|
90
|
+
CONFIG_FILE.write_text(json.dumps(config_dict, indent=4))
|
|
63
91
|
|
|
64
92
|
|
|
65
93
|
conf = QuantizerConfig()
|
|
@@ -69,8 +97,11 @@ atexit.register(conf.save)
|
|
|
69
97
|
def _process_gguf_stem(filename: str) -> str:
|
|
70
98
|
"""处理文件名, 移除可能的F16后缀.
|
|
71
99
|
|
|
100
|
+
Args:
|
|
101
|
+
filename: 输入的文件名(不含扩展名)
|
|
102
|
+
|
|
72
103
|
Returns:
|
|
73
|
-
str:
|
|
104
|
+
str: 处理后的文件名, 移除了F16后缀(如果存在)
|
|
74
105
|
"""
|
|
75
106
|
if filename.upper().endswith("-F16"):
|
|
76
107
|
filename = filename[:-4] # 移除-F16后缀
|
|
@@ -78,7 +109,13 @@ def _process_gguf_stem(filename: str) -> str:
|
|
|
78
109
|
|
|
79
110
|
|
|
80
111
|
class QuantizationWorker(QThread):
|
|
81
|
-
"""量化执行线程Worker.
|
|
112
|
+
"""量化执行线程Worker.
|
|
113
|
+
|
|
114
|
+
Attributes:
|
|
115
|
+
progress_msg_updated: 进度消息更新信号
|
|
116
|
+
progress_count_updated: 进度数值更新信号
|
|
117
|
+
is_finished: 完成信号
|
|
118
|
+
"""
|
|
82
119
|
|
|
83
120
|
progress_msg_updated = Signal(str)
|
|
84
121
|
progress_count_updated = Signal(int)
|
|
@@ -89,6 +126,12 @@ class QuantizationWorker(QThread):
|
|
|
89
126
|
input_file: pathlib.Path,
|
|
90
127
|
quant_types: list[str],
|
|
91
128
|
) -> None:
|
|
129
|
+
"""初始化量化Worker.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
input_file: 输入的F16 GGUF文件路径
|
|
133
|
+
quant_types: 需要转换的量化类型列表
|
|
134
|
+
"""
|
|
92
135
|
super().__init__()
|
|
93
136
|
|
|
94
137
|
self.input_file = input_file
|
|
@@ -111,7 +154,7 @@ class QuantizationWorker(QThread):
|
|
|
111
154
|
f"正在转换到 {quant_type} 格式...",
|
|
112
155
|
)
|
|
113
156
|
|
|
114
|
-
#
|
|
157
|
+
# 构建命令行参数
|
|
115
158
|
cmd = [
|
|
116
159
|
"llama-quantize",
|
|
117
160
|
str(self.input_file.name),
|
|
@@ -119,7 +162,7 @@ class QuantizationWorker(QThread):
|
|
|
119
162
|
quant_type,
|
|
120
163
|
]
|
|
121
164
|
|
|
122
|
-
#
|
|
165
|
+
# 执行转换命令
|
|
123
166
|
try:
|
|
124
167
|
process = subprocess.Popen(
|
|
125
168
|
cmd,
|
|
@@ -284,7 +327,7 @@ class GGUFQuantizerGUI(QMainWindow):
|
|
|
284
327
|
self.setCentralWidget(main_widget)
|
|
285
328
|
|
|
286
329
|
def select_file(self) -> None:
|
|
287
|
-
"""
|
|
330
|
+
"""选择F16 GGUF文件."""
|
|
288
331
|
# 使用上次选择的目录作为初始目录
|
|
289
332
|
initial_dir = ""
|
|
290
333
|
if conf.LAST_INPUT_FILE and pathlib.Path(conf.LAST_INPUT_FILE).exists():
|
|
@@ -333,21 +376,25 @@ class GGUFQuantizerGUI(QMainWindow):
|
|
|
333
376
|
filename = f"{_process_gguf_stem(self.input_file.stem)}-{quant_type}.gguf"
|
|
334
377
|
expected_file = dir_path / filename
|
|
335
378
|
if expected_file.exists():
|
|
336
|
-
#
|
|
379
|
+
# 文件已存在,标记并禁用,防止重复生成
|
|
337
380
|
self.quant_checks[quant_type].setText(
|
|
338
381
|
f"{self.quant_types[quant_type]} (已存在)",
|
|
339
382
|
)
|
|
340
383
|
self.quant_checks[quant_type].setStyleSheet("color: orange;")
|
|
384
|
+
self.quant_checks[quant_type].setChecked(True)
|
|
385
|
+
self.quant_checks[quant_type].setEnabled(False)
|
|
341
386
|
else:
|
|
342
387
|
self.quant_checks[quant_type].setText(
|
|
343
388
|
self.quant_types[quant_type],
|
|
344
389
|
)
|
|
345
390
|
self.quant_checks[quant_type].setStyleSheet("")
|
|
391
|
+
self.quant_checks[quant_type].setEnabled(True)
|
|
346
392
|
|
|
347
393
|
def _scroll_to_bottom(self) -> None:
|
|
348
394
|
"""滚动输出框到底部."""
|
|
349
395
|
scrollbar = self.output_text.verticalScrollBar()
|
|
350
|
-
scrollbar
|
|
396
|
+
if scrollbar:
|
|
397
|
+
scrollbar.setValue(scrollbar.maximum())
|
|
351
398
|
|
|
352
399
|
def on_quant_type_changed(self, _state: int) -> None:
|
|
353
400
|
"""量化类型变更时保存配置."""
|
|
@@ -369,7 +416,7 @@ class GGUFQuantizerGUI(QMainWindow):
|
|
|
369
416
|
return super().resizeEvent(event)
|
|
370
417
|
|
|
371
418
|
def start_conversion(self) -> None:
|
|
372
|
-
"""
|
|
419
|
+
"""开始转换量化任务."""
|
|
373
420
|
# 检查是否已有任务在运行
|
|
374
421
|
if self.worker and self.worker.isRunning():
|
|
375
422
|
self.output_text.append("已有转换任务正在进行, 请等待完成")
|
|
@@ -377,7 +424,9 @@ class GGUFQuantizerGUI(QMainWindow):
|
|
|
377
424
|
return
|
|
378
425
|
|
|
379
426
|
selected_quants: list[str] = [
|
|
380
|
-
q
|
|
427
|
+
q
|
|
428
|
+
for q, check in self.quant_checks.items()
|
|
429
|
+
if check.isChecked() and check.isEnabled()
|
|
381
430
|
]
|
|
382
431
|
|
|
383
432
|
if not selected_quants:
|
|
@@ -390,19 +439,8 @@ class GGUFQuantizerGUI(QMainWindow):
|
|
|
390
439
|
self._scroll_to_bottom()
|
|
391
440
|
return
|
|
392
441
|
|
|
393
|
-
#
|
|
394
|
-
|
|
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()
|
|
442
|
+
# 注意:由于selected_quants只包含启用的复选框,
|
|
443
|
+
# 所以不会包含已存在的禁用文件,无需额外检查覆盖
|
|
406
444
|
|
|
407
445
|
self.convert_btn.setEnabled(False)
|
|
408
446
|
self.progress_bar.setValue(0)
|
|
@@ -459,6 +497,7 @@ class GGUFQuantizerGUI(QMainWindow):
|
|
|
459
497
|
|
|
460
498
|
|
|
461
499
|
def main() -> None:
|
|
500
|
+
"""主程序入口."""
|
|
462
501
|
app = QApplication(sys.argv)
|
|
463
502
|
|
|
464
503
|
# 检查是否安装了llama.cpp
|
|
@@ -469,8 +508,8 @@ def main() -> None:
|
|
|
469
508
|
check=False,
|
|
470
509
|
)
|
|
471
510
|
except FileNotFoundError:
|
|
472
|
-
logger.
|
|
473
|
-
logger.
|
|
511
|
+
logger.error("错误: 未找到llama.cpp/quantize工具")
|
|
512
|
+
logger.error(
|
|
474
513
|
"请确保已编译llama.cpp并将quantize工具放在llama.cpp/目录下",
|
|
475
514
|
)
|
|
476
515
|
sys.exit(1)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
sfi/llmserver/llmserver.py
CHANGED
|
@@ -6,8 +6,8 @@ import logging
|
|
|
6
6
|
import os
|
|
7
7
|
import pathlib
|
|
8
8
|
import sys
|
|
9
|
+
from dataclasses import dataclass
|
|
9
10
|
from pathlib import Path
|
|
10
|
-
from types import SimpleNamespace
|
|
11
11
|
from typing import ClassVar
|
|
12
12
|
|
|
13
13
|
from PySide2.QtCore import QProcess, QTextStream, QUrl
|
|
@@ -36,36 +36,61 @@ from PySide2.QtWidgets import (
|
|
|
36
36
|
QWidget,
|
|
37
37
|
)
|
|
38
38
|
|
|
39
|
-
CONFIG_FILE = Path.home() / ".
|
|
39
|
+
CONFIG_FILE = Path.home() / ".pysfi" / "llmserver.json"
|
|
40
40
|
logging.basicConfig(level="INFO", format="%(message)s")
|
|
41
41
|
logger = logging.getLogger(__name__)
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
@dataclass
|
|
45
|
+
class LLMServerConfig:
|
|
45
46
|
"""Llama local model server configuration."""
|
|
46
47
|
|
|
47
48
|
TITLE: str = "Llama Local Model Server"
|
|
48
49
|
WIN_SIZE: ClassVar[list[int]] = [800, 800]
|
|
49
50
|
WIN_POS: ClassVar[list[int]] = [200, 200]
|
|
50
51
|
MODEL_PATH: str = ""
|
|
51
|
-
|
|
52
52
|
URL: str = "http://127.0.0.1"
|
|
53
53
|
LISTEN_PORT: int = 8080
|
|
54
54
|
LISTEN_PORT_RNG: ClassVar[list[int]] = [1024, 65535]
|
|
55
55
|
THREAD_COUNT_RNG: ClassVar[list[int]] = [1, 24]
|
|
56
56
|
THREAD_COUNT: int = 4
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
_loaded_from_file: bool = False
|
|
59
|
+
|
|
60
|
+
def __post_init__(self) -> None:
|
|
59
61
|
if CONFIG_FILE.exists():
|
|
60
62
|
logger.info("Loading configuration from %s", CONFIG_FILE)
|
|
61
|
-
|
|
63
|
+
try:
|
|
64
|
+
config_data = json.loads(CONFIG_FILE.read_text())
|
|
65
|
+
# Update instance attributes with loaded values
|
|
66
|
+
for key, value in config_data.items():
|
|
67
|
+
if hasattr(self, key):
|
|
68
|
+
setattr(self, key, value)
|
|
69
|
+
self._loaded_from_file = True
|
|
70
|
+
except (json.JSONDecodeError, TypeError, AttributeError) as e:
|
|
71
|
+
logger.error(f"Error loading config from {CONFIG_FILE}: {e}")
|
|
62
72
|
else:
|
|
63
73
|
logger.info("Using default configuration")
|
|
64
74
|
|
|
65
75
|
def save(self) -> None:
|
|
66
76
|
"""Save configuration."""
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
try:
|
|
78
|
+
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
# Convert dataclass to dict for JSON serialization
|
|
80
|
+
config_dict = {}
|
|
81
|
+
for attr_name in dir(self):
|
|
82
|
+
if not attr_name.startswith("_"):
|
|
83
|
+
try:
|
|
84
|
+
attr_value = getattr(self, attr_name)
|
|
85
|
+
if not callable(attr_value):
|
|
86
|
+
config_dict[attr_name] = attr_value
|
|
87
|
+
except AttributeError:
|
|
88
|
+
continue
|
|
89
|
+
CONFIG_FILE.write_text(json.dumps(config_dict, indent=4))
|
|
90
|
+
logger.info(f"Configuration saved to {CONFIG_FILE}")
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.error(f"Failed to save configuration: {e}")
|
|
93
|
+
raise
|
|
69
94
|
|
|
70
95
|
|
|
71
96
|
conf = LLMServerConfig()
|
|
@@ -92,11 +117,8 @@ class LlamaServerGUI(QMainWindow):
|
|
|
92
117
|
self.init_ui()
|
|
93
118
|
self.setup_process()
|
|
94
119
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
self.model_path_input.setText(str(model_path))
|
|
98
|
-
else:
|
|
99
|
-
self.model_path_input.setPlaceholderText("Choose model file...")
|
|
120
|
+
# Apply loaded configuration to UI controls
|
|
121
|
+
self.apply_config_to_ui()
|
|
100
122
|
|
|
101
123
|
def init_ui(self) -> None:
|
|
102
124
|
"""Initialize user interface."""
|
|
@@ -191,6 +213,19 @@ class LlamaServerGUI(QMainWindow):
|
|
|
191
213
|
self.process.readyReadStandardError.connect(self.handle_stderr) # type: ignore
|
|
192
214
|
self.process.finished.connect(self.on_process_finished) # type: ignore
|
|
193
215
|
|
|
216
|
+
def apply_config_to_ui(self) -> None:
|
|
217
|
+
"""Apply loaded configuration to UI controls."""
|
|
218
|
+
# Set model path
|
|
219
|
+
model_path = conf.MODEL_PATH
|
|
220
|
+
if model_path:
|
|
221
|
+
self.model_path_input.setText(str(model_path))
|
|
222
|
+
else:
|
|
223
|
+
self.model_path_input.setPlaceholderText("Choose model file...")
|
|
224
|
+
|
|
225
|
+
# Set port and thread values
|
|
226
|
+
self.port_spin.setValue(conf.LISTEN_PORT)
|
|
227
|
+
self.threads_spin.setValue(conf.THREAD_COUNT)
|
|
228
|
+
|
|
194
229
|
def on_config_changed(self) -> None:
|
|
195
230
|
"""Configuration changed."""
|
|
196
231
|
conf.MODEL_PATH = self.model_path_input.text().strip()
|
|
@@ -325,6 +360,21 @@ class LlamaServerGUI(QMainWindow):
|
|
|
325
360
|
conf.WIN_SIZE = [geometry.width(), geometry.height()]
|
|
326
361
|
return super().resizeEvent(event)
|
|
327
362
|
|
|
363
|
+
def closeEvent(self, event) -> None: # noqa: N802
|
|
364
|
+
"""Handle window close event to ensure configuration is saved."""
|
|
365
|
+
try:
|
|
366
|
+
# Save current configuration
|
|
367
|
+
conf.save()
|
|
368
|
+
logger.info("Configuration saved successfully on exit")
|
|
369
|
+
except Exception as e:
|
|
370
|
+
logger.error(f"Failed to save configuration on exit: {e}")
|
|
371
|
+
finally:
|
|
372
|
+
# Also stop server if running
|
|
373
|
+
if hasattr(self, "process") and self.process.state() == QProcess.Running:
|
|
374
|
+
self.process.terminate()
|
|
375
|
+
self.process.waitForFinished(2000) # Wait up to 2 seconds
|
|
376
|
+
event.accept()
|
|
377
|
+
|
|
328
378
|
|
|
329
379
|
def main() -> None:
|
|
330
380
|
"""Main entry point."""
|