pysfi 0.1.13__py3-none-any.whl → 0.1.15__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.13.dist-info → pysfi-0.1.15.dist-info}/METADATA +1 -1
- {pysfi-0.1.13.dist-info → pysfi-0.1.15.dist-info}/RECORD +35 -35
- {pysfi-0.1.13.dist-info → pysfi-0.1.15.dist-info}/entry_points.txt +2 -0
- sfi/__init__.py +20 -5
- sfi/alarmclock/__init__.py +3 -3
- sfi/bumpversion/__init__.py +5 -5
- sfi/bumpversion/bumpversion.py +64 -15
- sfi/cleanbuild/__init__.py +3 -3
- sfi/cleanbuild/cleanbuild.py +5 -1
- sfi/cli.py +13 -2
- sfi/condasetup/__init__.py +1 -1
- sfi/condasetup/condasetup.py +91 -76
- sfi/docdiff/__init__.py +1 -1
- sfi/docdiff/docdiff.py +3 -2
- sfi/docscan/__init__.py +3 -3
- sfi/docscan/docscan.py +78 -23
- sfi/docscan/docscan_gui.py +5 -5
- sfi/filedate/filedate.py +12 -5
- sfi/img2pdf/img2pdf.py +5 -5
- sfi/llmquantize/llmquantize.py +44 -33
- sfi/llmserver/__init__.py +1 -1
- sfi/makepython/makepython.py +880 -319
- sfi/pdfcrypt/__init__.py +30 -0
- sfi/pdfcrypt/pdfcrypt.py +435 -0
- sfi/pdfsplit/pdfsplit.py +45 -12
- sfi/pyarchive/__init__.py +1 -1
- sfi/pyarchive/pyarchive.py +1 -1
- sfi/pyembedinstall/pyembedinstall.py +1 -1
- sfi/pylibpack/pylibpack.py +5 -13
- sfi/pyloadergen/pyloadergen.py +6 -3
- sfi/pypack/pypack.py +131 -105
- sfi/pyprojectparse/pyprojectparse.py +19 -44
- sfi/pysourcepack/__init__.py +1 -1
- sfi/pysourcepack/pysourcepack.py +11 -14
- sfi/workflowengine/__init__.py +0 -0
- sfi/workflowengine/workflowengine.py +0 -547
- {pysfi-0.1.13.dist-info → pysfi-0.1.15.dist-info}/WHEEL +0 -0
sfi/docscan/docscan.py
CHANGED
|
@@ -91,7 +91,9 @@ def t(key: str, **kwargs) -> str:
|
|
|
91
91
|
Returns:
|
|
92
92
|
Translated text
|
|
93
93
|
"""
|
|
94
|
-
text =
|
|
94
|
+
text = (
|
|
95
|
+
ZH_TRANSLATIONS.get(key, key) if USE_CHINESE else EN_TRANSLATIONS.get(key, key)
|
|
96
|
+
)
|
|
95
97
|
|
|
96
98
|
# Format with kwargs if provided
|
|
97
99
|
if kwargs:
|
|
@@ -123,7 +125,9 @@ class Rule:
|
|
|
123
125
|
# Use re.ASCII for faster matching when possible
|
|
124
126
|
self.compiled_pattern = re.compile(self.pattern, flags | re.ASCII)
|
|
125
127
|
except re.error as e:
|
|
126
|
-
logger.warning(
|
|
128
|
+
logger.warning(
|
|
129
|
+
t("invalid_regex_pattern", pattern=self.pattern, error=e)
|
|
130
|
+
)
|
|
127
131
|
self.compiled_pattern = None
|
|
128
132
|
else:
|
|
129
133
|
self.compiled_pattern = None
|
|
@@ -274,13 +278,18 @@ class DocumentScanner:
|
|
|
274
278
|
"use_pdf_ocr": self.use_pdf_ocr,
|
|
275
279
|
"use_process_pool": self.use_process_pool,
|
|
276
280
|
},
|
|
277
|
-
"rules": [
|
|
281
|
+
"rules": [
|
|
282
|
+
{"name": r.name, "pattern": r.pattern, "is_regex": r.is_regex}
|
|
283
|
+
for r in self.rules
|
|
284
|
+
],
|
|
278
285
|
"matches": [],
|
|
279
286
|
}
|
|
280
287
|
|
|
281
288
|
# Scan files in parallel
|
|
282
289
|
processed = 0
|
|
283
|
-
executor_class =
|
|
290
|
+
executor_class = (
|
|
291
|
+
ProcessPoolExecutor if self.use_process_pool else ThreadPoolExecutor
|
|
292
|
+
)
|
|
284
293
|
executor = executor_class(max_workers=threads)
|
|
285
294
|
self._executor = executor # Keep reference for forced shutdown
|
|
286
295
|
|
|
@@ -350,10 +359,17 @@ class DocumentScanner:
|
|
|
350
359
|
break
|
|
351
360
|
|
|
352
361
|
try:
|
|
353
|
-
file_result = future.result(
|
|
362
|
+
file_result = future.result(
|
|
363
|
+
timeout=1.0
|
|
364
|
+
) # Short timeout to allow quick stop
|
|
354
365
|
if file_result and file_result["matches"]:
|
|
355
366
|
results["matches"].append(file_result)
|
|
356
|
-
logger.info(
|
|
367
|
+
logger.info(
|
|
368
|
+
t(
|
|
369
|
+
"found_matches_in_file",
|
|
370
|
+
file_name=Path(file_result.get("file_path", "")).name,
|
|
371
|
+
)
|
|
372
|
+
)
|
|
357
373
|
except TimeoutError:
|
|
358
374
|
logger.warning(t("task_timeout_scan_may_be_stopping"))
|
|
359
375
|
if self.stopped:
|
|
@@ -366,7 +382,9 @@ class DocumentScanner:
|
|
|
366
382
|
|
|
367
383
|
# Report progress
|
|
368
384
|
if show_progress and processed % 10 == 0:
|
|
369
|
-
logger.info(
|
|
385
|
+
logger.info(
|
|
386
|
+
t("progress_report", processed=processed, total=len(files))
|
|
387
|
+
)
|
|
370
388
|
|
|
371
389
|
# Call progress callback if set
|
|
372
390
|
if self._progress_callback:
|
|
@@ -391,7 +409,9 @@ class DocumentScanner:
|
|
|
391
409
|
if self.stopped:
|
|
392
410
|
logger.info(t("scan_stopped_processed_files", processed=processed))
|
|
393
411
|
else:
|
|
394
|
-
logger.info(
|
|
412
|
+
logger.info(
|
|
413
|
+
t("scan_complete_found_matches", matches_count=len(results["matches"]))
|
|
414
|
+
)
|
|
395
415
|
|
|
396
416
|
return results
|
|
397
417
|
|
|
@@ -493,7 +513,9 @@ class DocumentScanner:
|
|
|
493
513
|
return {}
|
|
494
514
|
|
|
495
515
|
except Exception as e:
|
|
496
|
-
logger.warning(
|
|
516
|
+
logger.warning(
|
|
517
|
+
t("could_not_extract_text_from_file", file_path=file_path, error=e)
|
|
518
|
+
)
|
|
497
519
|
return {}
|
|
498
520
|
|
|
499
521
|
processing_time = time.perf_counter() - file_start_time
|
|
@@ -549,14 +571,18 @@ class DocumentScanner:
|
|
|
549
571
|
try:
|
|
550
572
|
return self._extract_pdf_fitz(file_path)
|
|
551
573
|
except Exception as e:
|
|
552
|
-
logger.warning(
|
|
574
|
+
logger.warning(
|
|
575
|
+
t("pymupdf_failed_for_file", file_name=file_path.name, error=e)
|
|
576
|
+
)
|
|
553
577
|
|
|
554
578
|
# Fallback to pypdf
|
|
555
579
|
if pypdf is not None:
|
|
556
580
|
try:
|
|
557
581
|
return self._extract_pdf_pypdf(file_path)
|
|
558
582
|
except Exception as e:
|
|
559
|
-
logger.error(
|
|
583
|
+
logger.error(
|
|
584
|
+
t("pypdf_also_failed_for_file", file_name=file_path.name, error=e)
|
|
585
|
+
)
|
|
560
586
|
return "", {}
|
|
561
587
|
|
|
562
588
|
logger.warning(t("no_pdf_library_installed"))
|
|
@@ -632,7 +658,9 @@ class DocumentScanner:
|
|
|
632
658
|
except Exception as e:
|
|
633
659
|
if doc:
|
|
634
660
|
doc.close()
|
|
635
|
-
logger.warning(
|
|
661
|
+
logger.warning(
|
|
662
|
+
t("pymupdf_error_trying_fallback", file_path=file_path, error=e)
|
|
663
|
+
)
|
|
636
664
|
# Re-raise to trigger fallback to pypdf
|
|
637
665
|
raise
|
|
638
666
|
|
|
@@ -764,8 +792,12 @@ class DocumentScanner:
|
|
|
764
792
|
text_parts.append(text)
|
|
765
793
|
|
|
766
794
|
metadata = {
|
|
767
|
-
"title": book.get_metadata("DC", "title")[0][0]
|
|
768
|
-
|
|
795
|
+
"title": book.get_metadata("DC", "title")[0][0]
|
|
796
|
+
if book.get_metadata("DC", "title")
|
|
797
|
+
else "", # pyright: ignore[reportAttributeAccessIssue]
|
|
798
|
+
"author": book.get_metadata("DC", "creator")[0][0]
|
|
799
|
+
if book.get_metadata("DC", "creator")
|
|
800
|
+
else "", # pyright: ignore[reportAttributeAccessIssue]
|
|
769
801
|
"format": "EPUB",
|
|
770
802
|
}
|
|
771
803
|
|
|
@@ -810,7 +842,9 @@ class DocumentScanner:
|
|
|
810
842
|
root = tree.getroot()
|
|
811
843
|
|
|
812
844
|
# Extract all text content
|
|
813
|
-
text_parts = [
|
|
845
|
+
text_parts = [
|
|
846
|
+
elem.text for elem in root.iter() if elem.text and elem.text.strip()
|
|
847
|
+
]
|
|
814
848
|
text = "\n".join(text_parts)
|
|
815
849
|
|
|
816
850
|
metadata = {
|
|
@@ -954,7 +988,9 @@ class DocumentScanner:
|
|
|
954
988
|
wb.close()
|
|
955
989
|
return "", {}
|
|
956
990
|
|
|
957
|
-
row_text = " | ".join(
|
|
991
|
+
row_text = " | ".join(
|
|
992
|
+
str(cell) if cell is not None else "" for cell in row
|
|
993
|
+
)
|
|
958
994
|
if row_text.strip():
|
|
959
995
|
text_parts.append(row_text)
|
|
960
996
|
|
|
@@ -1017,7 +1053,9 @@ class DocumentScanner:
|
|
|
1017
1053
|
|
|
1018
1054
|
return text, metadata
|
|
1019
1055
|
except Exception as e:
|
|
1020
|
-
logger.warning(
|
|
1056
|
+
logger.warning(
|
|
1057
|
+
t("could_not_perform_ocr_on_file", file_path=file_path, error=e)
|
|
1058
|
+
)
|
|
1021
1059
|
return "", {}
|
|
1022
1060
|
|
|
1023
1061
|
def _extract_text(self, file_path: Path) -> tuple[str, dict[str, Any]]:
|
|
@@ -1047,8 +1085,12 @@ def main():
|
|
|
1047
1085
|
USE_CHINESE = temp_args.lang == "zh"
|
|
1048
1086
|
|
|
1049
1087
|
parser = argparse.ArgumentParser(description=t("document_scanner_description"))
|
|
1050
|
-
parser.add_argument(
|
|
1051
|
-
|
|
1088
|
+
parser.add_argument(
|
|
1089
|
+
"input", type=str, nargs="?", default=str(cwd), help=t("input_directory_help")
|
|
1090
|
+
)
|
|
1091
|
+
parser.add_argument(
|
|
1092
|
+
"-r", "--rules", type=str, default="rules.json", help=t("rules_file_help")
|
|
1093
|
+
)
|
|
1052
1094
|
parser.add_argument("--recursive", action="store_true", help=t("recursive_help"))
|
|
1053
1095
|
parser.add_argument(
|
|
1054
1096
|
"-f",
|
|
@@ -1056,7 +1098,9 @@ def main():
|
|
|
1056
1098
|
help=t("file_types_help"),
|
|
1057
1099
|
default="pdf,docx,xlsx,pptx,txt,odt,rtf,epub,csv,xml,html,md,jpg,jpeg,png,gif,bmp,tiff",
|
|
1058
1100
|
)
|
|
1059
|
-
parser.add_argument(
|
|
1101
|
+
parser.add_argument(
|
|
1102
|
+
"--use-pdf-ocr", help=t("use_pdf_ocr_help"), action="store_true"
|
|
1103
|
+
)
|
|
1060
1104
|
parser.add_argument(
|
|
1061
1105
|
"--use-process-pool",
|
|
1062
1106
|
help=t("use_process_pool_help"),
|
|
@@ -1074,7 +1118,9 @@ def main():
|
|
|
1074
1118
|
parser.add_argument("-v", "--verbose", help=t("verbose_help"), action="store_true")
|
|
1075
1119
|
|
|
1076
1120
|
# 添加语言参数
|
|
1077
|
-
parser.add_argument(
|
|
1121
|
+
parser.add_argument(
|
|
1122
|
+
"--lang", help=t("language_help"), choices=["en", "zh"], default="zh"
|
|
1123
|
+
)
|
|
1078
1124
|
|
|
1079
1125
|
args = parser.parse_args()
|
|
1080
1126
|
|
|
@@ -1129,11 +1175,20 @@ def main():
|
|
|
1129
1175
|
file_types = [ft.strip() for ft in args.file_types.split(",")]
|
|
1130
1176
|
|
|
1131
1177
|
# Create scanner and run scan
|
|
1132
|
-
scanner = DocumentScanner(
|
|
1178
|
+
scanner = DocumentScanner(
|
|
1179
|
+
input_dir,
|
|
1180
|
+
rules,
|
|
1181
|
+
file_types,
|
|
1182
|
+
args.use_pdf_ocr,
|
|
1183
|
+
args.use_process_pool,
|
|
1184
|
+
args.batch_size,
|
|
1185
|
+
)
|
|
1133
1186
|
results = scanner.scan(threads=args.threads, show_progress=args.progress)
|
|
1134
1187
|
|
|
1135
1188
|
# Save results to JSON file in input directory
|
|
1136
|
-
output_file =
|
|
1189
|
+
output_file = (
|
|
1190
|
+
input_dir / f"scan_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
1191
|
+
)
|
|
1137
1192
|
with open(output_file, "w", encoding="utf-8") as f:
|
|
1138
1193
|
json.dump(results, f, indent=2, ensure_ascii=False)
|
|
1139
1194
|
|
sfi/docscan/docscan_gui.py
CHANGED
|
@@ -45,14 +45,14 @@ except ImportError:
|
|
|
45
45
|
try:
|
|
46
46
|
from sfi.docscan.docscan import DocumentScanner, Rule
|
|
47
47
|
except ImportError:
|
|
48
|
-
from
|
|
48
|
+
from docscan.docscan import DocumentScanner, Rule
|
|
49
49
|
|
|
50
50
|
# Import translations
|
|
51
51
|
try:
|
|
52
52
|
from sfi.docscan.lang.zhcn import TRANSLATIONS
|
|
53
53
|
except ImportError:
|
|
54
54
|
try:
|
|
55
|
-
from
|
|
55
|
+
from docscan.lang.zhcn import TRANSLATIONS
|
|
56
56
|
except ImportError:
|
|
57
57
|
TRANSLATIONS = {}
|
|
58
58
|
|
|
@@ -413,15 +413,15 @@ class SettingsDialog(QDialog):
|
|
|
413
413
|
if item.widget() and isinstance(item.widget(), QGroupBox):
|
|
414
414
|
group_box = item.widget()
|
|
415
415
|
if "Language" in group_box.title(): # type: ignore
|
|
416
|
-
group_box.
|
|
416
|
+
group_box.setWindowTitle(
|
|
417
417
|
t("language_settings", default="Language Settings")
|
|
418
418
|
) # type: ignore
|
|
419
419
|
elif "Processing" in group_box.title(): # type: ignore
|
|
420
|
-
group_box.
|
|
420
|
+
group_box.setWindowTitle(
|
|
421
421
|
t("processing_options", default="Processing Options")
|
|
422
422
|
) # type: ignore
|
|
423
423
|
elif "Performance" in group_box.title(): # type: ignore
|
|
424
|
-
group_box.
|
|
424
|
+
group_box.setWindowTitle(
|
|
425
425
|
t("performance_settings", default="Performance Settings")
|
|
426
426
|
) # type: ignore
|
|
427
427
|
|
sfi/filedate/filedate.py
CHANGED
|
@@ -11,11 +11,18 @@ import time
|
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from functools import cached_property, lru_cache
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
from re import Pattern
|
|
15
|
+
from typing import Final
|
|
16
|
+
|
|
17
|
+
# Configuration constants
|
|
18
|
+
DETECT_SEPARATORS: Final[str] = "-_#.~"
|
|
19
|
+
SEP: Final[str] = "_"
|
|
20
|
+
MAX_RETRY: Final[int] = 100
|
|
21
|
+
|
|
22
|
+
# Date pattern for detection
|
|
23
|
+
DATE_PATTERN: Final[Pattern[str]] = re.compile(
|
|
24
|
+
r"(20|19)\d{2}((0[1-9])|(1[012]))((0[1-9])|([12]\d)|(3[01]))"
|
|
25
|
+
)
|
|
19
26
|
|
|
20
27
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
21
28
|
logger = logging.getLogger(__name__)
|
sfi/img2pdf/img2pdf.py
CHANGED
|
@@ -33,7 +33,7 @@ class ImageToPdfConfig:
|
|
|
33
33
|
"""Configuration for image to PDF conversion."""
|
|
34
34
|
|
|
35
35
|
DPI: int = 300
|
|
36
|
-
EXTENSIONS: set[str] = None
|
|
36
|
+
EXTENSIONS: set[str] | None = None
|
|
37
37
|
|
|
38
38
|
def __post_init__(self) -> None:
|
|
39
39
|
# Initialize default extensions if not provided
|
|
@@ -68,7 +68,7 @@ class ImageToPdfConfig:
|
|
|
68
68
|
def save(self) -> None:
|
|
69
69
|
"""Save current configuration to file."""
|
|
70
70
|
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
-
config_dict = {"DPI": self.DPI, "EXTENSIONS": list(self.EXTENSIONS)}
|
|
71
|
+
config_dict = {"DPI": self.DPI, "EXTENSIONS": list(self.EXTENSIONS or set())}
|
|
72
72
|
CONFIG_FILE.write_text(json.dumps(config_dict, indent=4), encoding="utf-8")
|
|
73
73
|
|
|
74
74
|
|
|
@@ -111,7 +111,7 @@ def is_valid_image(file_path: Path) -> bool:
|
|
|
111
111
|
|
|
112
112
|
# Extension validation.
|
|
113
113
|
ext = file_path.suffix.lower()
|
|
114
|
-
if ext not in conf.EXTENSIONS:
|
|
114
|
+
if not conf.EXTENSIONS or ext not in conf.EXTENSIONS:
|
|
115
115
|
logger.debug(f"Invalid image extension: {ext}, {file_path}")
|
|
116
116
|
return False
|
|
117
117
|
|
|
@@ -337,8 +337,8 @@ class ImageToPDFRunner:
|
|
|
337
337
|
del image
|
|
338
338
|
if "rgb_img" in locals():
|
|
339
339
|
del rgb_img
|
|
340
|
-
except:
|
|
341
|
-
|
|
340
|
+
except Exception:
|
|
341
|
+
logger.error(f"Cleanup image: {filepath} failed")
|
|
342
342
|
return None
|
|
343
343
|
|
|
344
344
|
def _auto_rotate_image(self, image: Image.Image) -> Image.Image:
|
sfi/llmquantize/llmquantize.py
CHANGED
|
@@ -31,9 +31,14 @@ from PySide2.QtWidgets import (
|
|
|
31
31
|
|
|
32
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
|
|
39
44
|
class QuantizerConfig:
|
|
@@ -44,9 +49,9 @@ class QuantizerConfig:
|
|
|
44
49
|
WIN_POS: list[int] = None
|
|
45
50
|
LAST_INPUT_FILE: str = ""
|
|
46
51
|
SELECTED_QUANTS: list[str] = None
|
|
47
|
-
_loaded_from_file: bool = False
|
|
48
52
|
|
|
49
53
|
def __post_init__(self) -> None:
|
|
54
|
+
"""初始化默认值并加载配置文件."""
|
|
50
55
|
# 初始化默认值
|
|
51
56
|
if self.WIN_SIZE is None:
|
|
52
57
|
self.WIN_SIZE = [600, 500]
|
|
@@ -62,12 +67,7 @@ class QuantizerConfig:
|
|
|
62
67
|
# 更新实例属性,只更新存在的属性
|
|
63
68
|
for key, value in config_data.items():
|
|
64
69
|
if hasattr(self, key):
|
|
65
|
-
|
|
66
|
-
# 对于列表类型,需要特别处理
|
|
67
|
-
setattr(self, key, value)
|
|
68
|
-
else:
|
|
69
|
-
setattr(self, key, value)
|
|
70
|
-
self._loaded_from_file = True
|
|
70
|
+
setattr(self, key, value)
|
|
71
71
|
except (json.JSONDecodeError, TypeError, AttributeError) as e:
|
|
72
72
|
logger.warning("Failed to load configuration: %s", e)
|
|
73
73
|
logger.info("Using default configuration")
|
|
@@ -75,7 +75,7 @@ class QuantizerConfig:
|
|
|
75
75
|
logger.info("Using default configuration")
|
|
76
76
|
|
|
77
77
|
def save(self) -> None:
|
|
78
|
-
"""
|
|
78
|
+
"""保存配置到文件."""
|
|
79
79
|
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
80
80
|
# 将数据类转换为字典进行JSON序列化
|
|
81
81
|
config_dict = {}
|
|
@@ -97,8 +97,11 @@ atexit.register(conf.save)
|
|
|
97
97
|
def _process_gguf_stem(filename: str) -> str:
|
|
98
98
|
"""处理文件名, 移除可能的F16后缀.
|
|
99
99
|
|
|
100
|
+
Args:
|
|
101
|
+
filename: 输入的文件名(不含扩展名)
|
|
102
|
+
|
|
100
103
|
Returns:
|
|
101
|
-
str:
|
|
104
|
+
str: 处理后的文件名, 移除了F16后缀(如果存在)
|
|
102
105
|
"""
|
|
103
106
|
if filename.upper().endswith("-F16"):
|
|
104
107
|
filename = filename[:-4] # 移除-F16后缀
|
|
@@ -106,7 +109,13 @@ def _process_gguf_stem(filename: str) -> str:
|
|
|
106
109
|
|
|
107
110
|
|
|
108
111
|
class QuantizationWorker(QThread):
|
|
109
|
-
"""量化执行线程Worker.
|
|
112
|
+
"""量化执行线程Worker.
|
|
113
|
+
|
|
114
|
+
Attributes:
|
|
115
|
+
progress_msg_updated: 进度消息更新信号
|
|
116
|
+
progress_count_updated: 进度数值更新信号
|
|
117
|
+
is_finished: 完成信号
|
|
118
|
+
"""
|
|
110
119
|
|
|
111
120
|
progress_msg_updated = Signal(str)
|
|
112
121
|
progress_count_updated = Signal(int)
|
|
@@ -117,6 +126,12 @@ class QuantizationWorker(QThread):
|
|
|
117
126
|
input_file: pathlib.Path,
|
|
118
127
|
quant_types: list[str],
|
|
119
128
|
) -> None:
|
|
129
|
+
"""初始化量化Worker.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
input_file: 输入的F16 GGUF文件路径
|
|
133
|
+
quant_types: 需要转换的量化类型列表
|
|
134
|
+
"""
|
|
120
135
|
super().__init__()
|
|
121
136
|
|
|
122
137
|
self.input_file = input_file
|
|
@@ -139,7 +154,7 @@ class QuantizationWorker(QThread):
|
|
|
139
154
|
f"正在转换到 {quant_type} 格式...",
|
|
140
155
|
)
|
|
141
156
|
|
|
142
|
-
#
|
|
157
|
+
# 构建命令行参数
|
|
143
158
|
cmd = [
|
|
144
159
|
"llama-quantize",
|
|
145
160
|
str(self.input_file.name),
|
|
@@ -147,7 +162,7 @@ class QuantizationWorker(QThread):
|
|
|
147
162
|
quant_type,
|
|
148
163
|
]
|
|
149
164
|
|
|
150
|
-
#
|
|
165
|
+
# 执行转换命令
|
|
151
166
|
try:
|
|
152
167
|
process = subprocess.Popen(
|
|
153
168
|
cmd,
|
|
@@ -312,7 +327,7 @@ class GGUFQuantizerGUI(QMainWindow):
|
|
|
312
327
|
self.setCentralWidget(main_widget)
|
|
313
328
|
|
|
314
329
|
def select_file(self) -> None:
|
|
315
|
-
"""
|
|
330
|
+
"""选择F16 GGUF文件."""
|
|
316
331
|
# 使用上次选择的目录作为初始目录
|
|
317
332
|
initial_dir = ""
|
|
318
333
|
if conf.LAST_INPUT_FILE and pathlib.Path(conf.LAST_INPUT_FILE).exists():
|
|
@@ -361,21 +376,25 @@ class GGUFQuantizerGUI(QMainWindow):
|
|
|
361
376
|
filename = f"{_process_gguf_stem(self.input_file.stem)}-{quant_type}.gguf"
|
|
362
377
|
expected_file = dir_path / filename
|
|
363
378
|
if expected_file.exists():
|
|
364
|
-
#
|
|
379
|
+
# 文件已存在,标记并禁用,防止重复生成
|
|
365
380
|
self.quant_checks[quant_type].setText(
|
|
366
381
|
f"{self.quant_types[quant_type]} (已存在)",
|
|
367
382
|
)
|
|
368
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)
|
|
369
386
|
else:
|
|
370
387
|
self.quant_checks[quant_type].setText(
|
|
371
388
|
self.quant_types[quant_type],
|
|
372
389
|
)
|
|
373
390
|
self.quant_checks[quant_type].setStyleSheet("")
|
|
391
|
+
self.quant_checks[quant_type].setEnabled(True)
|
|
374
392
|
|
|
375
393
|
def _scroll_to_bottom(self) -> None:
|
|
376
394
|
"""滚动输出框到底部."""
|
|
377
395
|
scrollbar = self.output_text.verticalScrollBar()
|
|
378
|
-
scrollbar
|
|
396
|
+
if scrollbar:
|
|
397
|
+
scrollbar.setValue(scrollbar.maximum())
|
|
379
398
|
|
|
380
399
|
def on_quant_type_changed(self, _state: int) -> None:
|
|
381
400
|
"""量化类型变更时保存配置."""
|
|
@@ -397,7 +416,7 @@ class GGUFQuantizerGUI(QMainWindow):
|
|
|
397
416
|
return super().resizeEvent(event)
|
|
398
417
|
|
|
399
418
|
def start_conversion(self) -> None:
|
|
400
|
-
"""
|
|
419
|
+
"""开始转换量化任务."""
|
|
401
420
|
# 检查是否已有任务在运行
|
|
402
421
|
if self.worker and self.worker.isRunning():
|
|
403
422
|
self.output_text.append("已有转换任务正在进行, 请等待完成")
|
|
@@ -405,7 +424,9 @@ class GGUFQuantizerGUI(QMainWindow):
|
|
|
405
424
|
return
|
|
406
425
|
|
|
407
426
|
selected_quants: list[str] = [
|
|
408
|
-
q
|
|
427
|
+
q
|
|
428
|
+
for q, check in self.quant_checks.items()
|
|
429
|
+
if check.isChecked() and check.isEnabled()
|
|
409
430
|
]
|
|
410
431
|
|
|
411
432
|
if not selected_quants:
|
|
@@ -418,19 +439,8 @@ class GGUFQuantizerGUI(QMainWindow):
|
|
|
418
439
|
self._scroll_to_bottom()
|
|
419
440
|
return
|
|
420
441
|
|
|
421
|
-
#
|
|
422
|
-
|
|
423
|
-
for quant_type in selected_quants:
|
|
424
|
-
filename = f"{_process_gguf_stem(self.input_file.stem)}-{quant_type}.gguf"
|
|
425
|
-
expected_file = self.input_file.parent / filename
|
|
426
|
-
if expected_file.exists():
|
|
427
|
-
existing_files.append(filename)
|
|
428
|
-
|
|
429
|
-
if existing_files:
|
|
430
|
-
self.output_text.append("警告: 将覆盖以下已存在文件:")
|
|
431
|
-
for existing_file in existing_files:
|
|
432
|
-
self.output_text.append(f" - {existing_file}")
|
|
433
|
-
self._scroll_to_bottom()
|
|
442
|
+
# 注意:由于selected_quants只包含启用的复选框,
|
|
443
|
+
# 所以不会包含已存在的禁用文件,无需额外检查覆盖
|
|
434
444
|
|
|
435
445
|
self.convert_btn.setEnabled(False)
|
|
436
446
|
self.progress_bar.setValue(0)
|
|
@@ -487,6 +497,7 @@ class GGUFQuantizerGUI(QMainWindow):
|
|
|
487
497
|
|
|
488
498
|
|
|
489
499
|
def main() -> None:
|
|
500
|
+
"""主程序入口."""
|
|
490
501
|
app = QApplication(sys.argv)
|
|
491
502
|
|
|
492
503
|
# 检查是否安装了llama.cpp
|
|
@@ -497,8 +508,8 @@ def main() -> None:
|
|
|
497
508
|
check=False,
|
|
498
509
|
)
|
|
499
510
|
except FileNotFoundError:
|
|
500
|
-
logger.
|
|
501
|
-
logger.
|
|
511
|
+
logger.error("错误: 未找到llama.cpp/quantize工具")
|
|
512
|
+
logger.error(
|
|
502
513
|
"请确保已编译llama.cpp并将quantize工具放在llama.cpp/目录下",
|
|
503
514
|
)
|
|
504
515
|
sys.exit(1)
|
sfi/llmserver/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
|