pygpt-net 2.7.2__py3-none-any.whl → 2.7.4__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.
- pygpt_net/CHANGELOG.txt +12 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +382 -350
- pygpt_net/controller/chat/attachment.py +5 -1
- pygpt_net/controller/chat/image.py +40 -5
- pygpt_net/controller/files/files.py +3 -1
- pygpt_net/controller/layout/layout.py +2 -2
- pygpt_net/controller/media/media.py +70 -1
- pygpt_net/controller/theme/nodes.py +2 -1
- pygpt_net/controller/ui/mode.py +5 -1
- pygpt_net/controller/ui/ui.py +17 -2
- pygpt_net/core/filesystem/url.py +4 -1
- pygpt_net/core/render/web/helpers.py +5 -0
- pygpt_net/data/config/config.json +5 -4
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +0 -14
- pygpt_net/data/css/web-blocks.css +3 -0
- pygpt_net/data/css/web-chatgpt.css +3 -0
- pygpt_net/data/locale/locale.de.ini +6 -0
- pygpt_net/data/locale/locale.en.ini +7 -1
- pygpt_net/data/locale/locale.es.ini +6 -0
- pygpt_net/data/locale/locale.fr.ini +6 -0
- pygpt_net/data/locale/locale.it.ini +6 -0
- pygpt_net/data/locale/locale.pl.ini +7 -1
- pygpt_net/data/locale/locale.uk.ini +6 -0
- pygpt_net/data/locale/locale.zh.ini +6 -0
- pygpt_net/launcher.py +115 -55
- pygpt_net/preload.py +243 -0
- pygpt_net/provider/api/google/image.py +317 -10
- pygpt_net/provider/api/google/video.py +160 -4
- pygpt_net/provider/api/openai/image.py +201 -93
- pygpt_net/provider/api/openai/video.py +99 -24
- pygpt_net/provider/api/x_ai/image.py +25 -2
- pygpt_net/provider/core/config/patch.py +17 -1
- pygpt_net/ui/layout/chat/input.py +20 -2
- pygpt_net/ui/layout/chat/painter.py +6 -4
- pygpt_net/ui/layout/toolbox/image.py +21 -11
- pygpt_net/ui/layout/toolbox/raw.py +2 -2
- pygpt_net/ui/layout/toolbox/video.py +22 -9
- pygpt_net/ui/main.py +84 -3
- pygpt_net/ui/widget/dialog/base.py +3 -10
- pygpt_net/ui/widget/option/combo.py +119 -1
- pygpt_net/ui/widget/textarea/input_extra.py +664 -0
- {pygpt_net-2.7.2.dist-info → pygpt_net-2.7.4.dist-info}/METADATA +27 -20
- {pygpt_net-2.7.2.dist-info → pygpt_net-2.7.4.dist-info}/RECORD +48 -46
- {pygpt_net-2.7.2.dist-info → pygpt_net-2.7.4.dist-info}/LICENSE +0 -0
- {pygpt_net-2.7.2.dist-info → pygpt_net-2.7.4.dist-info}/WHEEL +0 -0
- {pygpt_net-2.7.2.dist-info → pygpt_net-2.7.4.dist-info}/entry_points.txt +0 -0
|
@@ -748,6 +748,8 @@ idx.token.warn = Це призведе до використання додат
|
|
|
748
748
|
img.action.open = Відкрити повний розмір
|
|
749
749
|
img.action.save = Зберегти як...
|
|
750
750
|
img.raw = Режим сирця
|
|
751
|
+
img.remix = Ремікс/Розширити
|
|
752
|
+
img.remix.tooltip = Увімкнути ремікс/розширення з попереднього зображення у контексті.\nЯкщо увімкнено, попереднє зображення буде використовуватися як довідник замість створення нового з нуля.
|
|
751
753
|
img.save.title = Зберегти зображення
|
|
752
754
|
img.status.downloading = Завантаження...
|
|
753
755
|
img.status.error = Помилка генерації зображення
|
|
@@ -771,6 +773,8 @@ input.search.placeholder = Пошук...
|
|
|
771
773
|
input.send_clear = Очистити після відправлення
|
|
772
774
|
input.stream = Потік
|
|
773
775
|
input.tab = Введення
|
|
776
|
+
input.tab.extra = Додатковий prompt
|
|
777
|
+
input.tab.extra.negative_prompt = Negative prompt
|
|
774
778
|
input.tab.tooltip = {chars} символів (~{tokens} токенів)
|
|
775
779
|
interpreter.all = Виконати історію (все)
|
|
776
780
|
interpreter.auto_clear = Очистити при відправці
|
|
@@ -1677,6 +1681,8 @@ updater.check.launch = Перевіряти при запуску
|
|
|
1677
1681
|
update.released = збірка
|
|
1678
1682
|
update.snap = Перейти до Snap Store
|
|
1679
1683
|
update.title = Перевірка оновлень
|
|
1684
|
+
video.remix = Ремікс/Розширити
|
|
1685
|
+
video.remix.tooltip = Увімкнути ремікс/розширення з попереднього відео у контексті (Sora2, Veo3.1).\nЯкщо увімкнено, попереднє відео буде використовуватися як довідник замість створення нового з нуля.
|
|
1680
1686
|
vid.status.downloading = Завантаження відео... будь ласка, зачекайте...
|
|
1681
1687
|
vid.status.generating = Генерація відео з
|
|
1682
1688
|
vid.status.prompt.error = Виникла помилка під час покращення запиту
|
|
@@ -748,6 +748,8 @@ idx.token.warn = 这将消耗额外的 token 来嵌入数据。
|
|
|
748
748
|
img.action.open = 打开全尺寸
|
|
749
749
|
img.action.save = 另存为...
|
|
750
750
|
img.raw = 原始模式
|
|
751
|
+
img.remix = 混音/扩展
|
|
752
|
+
img.remix.tooltip = 启用上下文中从上一个图像的混音/扩展。\n如果启用,将使用上一个图像作为参考,而不是从头创建一个新图像。
|
|
751
753
|
img.save.title = 保存图片
|
|
752
754
|
img.status.downloading = 正在下载...
|
|
753
755
|
img.status.error = 图像生成错误
|
|
@@ -771,6 +773,8 @@ input.search.placeholder = 搜索...
|
|
|
771
773
|
input.send_clear = 發送後清除
|
|
772
774
|
input.stream = 流
|
|
773
775
|
input.tab = 輸入
|
|
776
|
+
input.tab.extra = 额外提示
|
|
777
|
+
input.tab.extra.negative_prompt = Negative prompt
|
|
774
778
|
input.tab.tooltip = {chars} 字符 (~{tokens} 令牌)
|
|
775
779
|
interpreter.all = 执行历史记录(全部)
|
|
776
780
|
interpreter.auto_clear = 发送时清除
|
|
@@ -1677,6 +1681,8 @@ updater.check.launch = 啟動時檢查
|
|
|
1677
1681
|
update.released = 發布時間
|
|
1678
1682
|
update.snap = 前往Snap商店
|
|
1679
1683
|
update.title = 檢查更新中
|
|
1684
|
+
video.remix = 混音/扩展
|
|
1685
|
+
video.remix.tooltip = 启用上下文中从上一个视频的混音/扩展 (Sora2, Veo3.1)。\n如果启用,将使用上一个视频作为参考,而不是从头创建一个新视频。
|
|
1680
1686
|
vid.status.downloading = 正在下载视频...请稍候...
|
|
1681
1687
|
vid.status.generating = 正在从中生成视频
|
|
1682
1688
|
vid.status.prompt.error = 提示增强出错
|
pygpt_net/launcher.py
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.
|
|
9
|
+
# Updated Date: 2025.12.31 17:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import os
|
|
@@ -50,6 +50,31 @@ class Launcher:
|
|
|
50
50
|
self.force_disable_gpu = False
|
|
51
51
|
self.shortcut_filter = None
|
|
52
52
|
self.workdir = None
|
|
53
|
+
self._preloader = None
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _clean_multiprocessing_argv(argv):
|
|
57
|
+
"""
|
|
58
|
+
Clear multiprocessing flags from argv
|
|
59
|
+
|
|
60
|
+
:param argv: list of command line arguments
|
|
61
|
+
"""
|
|
62
|
+
skip_flags = {
|
|
63
|
+
"--multiprocessing-fork",
|
|
64
|
+
"--multiprocessing-spawn",
|
|
65
|
+
"--billiard-fork",
|
|
66
|
+
"--billiard-spawn",
|
|
67
|
+
}
|
|
68
|
+
skip_prefixes = (
|
|
69
|
+
"parent_pid=",
|
|
70
|
+
"pipe_handle=",
|
|
71
|
+
"forkserver_port=",
|
|
72
|
+
"forkserver_authkey=",
|
|
73
|
+
)
|
|
74
|
+
return [
|
|
75
|
+
a for a in argv
|
|
76
|
+
if a not in skip_flags and not any(a.startswith(p) for p in skip_prefixes)
|
|
77
|
+
]
|
|
53
78
|
|
|
54
79
|
def setup(self) -> dict:
|
|
55
80
|
"""
|
|
@@ -57,59 +82,65 @@ class Launcher:
|
|
|
57
82
|
|
|
58
83
|
:return: dict with launcher arguments
|
|
59
84
|
"""
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
85
|
+
args = {}
|
|
86
|
+
try:
|
|
87
|
+
parser = argparse.ArgumentParser()
|
|
88
|
+
parser.add_argument(
|
|
89
|
+
"-d",
|
|
90
|
+
"--debug",
|
|
91
|
+
required=False,
|
|
92
|
+
help="debug mode (0=disabled, 1=info, 2=debug)",
|
|
93
|
+
)
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"-l",
|
|
96
|
+
"--legacy",
|
|
97
|
+
required=False,
|
|
98
|
+
help="force enable legacy mode (0=disabled, 1=enable)",
|
|
99
|
+
)
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"-n",
|
|
102
|
+
"--disable-gpu",
|
|
103
|
+
required=False,
|
|
104
|
+
help="force disable OpenGL (1=disabled, 0=enabled)",
|
|
105
|
+
)
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
"-w",
|
|
108
|
+
"--workdir",
|
|
109
|
+
required=False,
|
|
110
|
+
help="force set workdir",
|
|
111
|
+
)
|
|
112
|
+
safe_argv = self._clean_multiprocessing_argv(sys.argv[1:])
|
|
113
|
+
known, unknown = parser.parse_known_args(safe_argv)
|
|
114
|
+
args = vars(known)
|
|
115
|
+
|
|
116
|
+
# set log level [ERROR|WARNING|INFO|DEBUG]
|
|
117
|
+
if "debug" in args and args["debug"] == "1":
|
|
118
|
+
print("** Debug mode enabled (1=INFO)")
|
|
119
|
+
Debug.init(INFO)
|
|
120
|
+
self.debug = True
|
|
121
|
+
elif "debug" in args and args["debug"] == "2":
|
|
122
|
+
print("** Debug mode enabled (2=DEBUG)")
|
|
123
|
+
Debug.init(DEBUG)
|
|
124
|
+
self.debug = True
|
|
125
|
+
else:
|
|
126
|
+
Debug.init(ERROR) # default log level
|
|
127
|
+
|
|
128
|
+
# force legacy mode
|
|
129
|
+
if "legacy" in args and args["legacy"] == "1":
|
|
130
|
+
print("** Force legacy mode enabled")
|
|
131
|
+
self.force_legacy = True
|
|
132
|
+
|
|
133
|
+
# force disable GPU
|
|
134
|
+
if "disable_gpu" in args and args["disable_gpu"] == "1":
|
|
135
|
+
print("** Force disable GPU enabled")
|
|
136
|
+
self.force_disable_gpu = True
|
|
137
|
+
|
|
138
|
+
# force set workdir
|
|
139
|
+
if "workdir" in args and args["workdir"] is not None:
|
|
140
|
+
# set as environment variable
|
|
141
|
+
os.environ["PYGPT_WORKDIR"] = args["workdir"]
|
|
142
|
+
except Exception as e:
|
|
143
|
+
print(f"** Launcher setup error: {e}")
|
|
113
144
|
|
|
114
145
|
return args
|
|
115
146
|
|
|
@@ -123,6 +154,13 @@ class Launcher:
|
|
|
123
154
|
self.window = MainWindow(self.app, args=args)
|
|
124
155
|
self.shortcut_filter = GlobalShortcutFilter(self.window)
|
|
125
156
|
|
|
157
|
+
# Connect the window "ready" signal to close the splash
|
|
158
|
+
if self._preloader is not None:
|
|
159
|
+
try:
|
|
160
|
+
self.window.appReady.connect(self._on_window_ready, Qt.QueuedConnection)
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
|
|
126
164
|
def handle_signal(self, signal_number, frame):
|
|
127
165
|
"""
|
|
128
166
|
Handle termination signal (SIGTERM, SIGINT)
|
|
@@ -136,6 +174,28 @@ class Launcher:
|
|
|
136
174
|
print("Shutting down...")
|
|
137
175
|
self.window.close()
|
|
138
176
|
|
|
177
|
+
def attach_preloader(self, preloader):
|
|
178
|
+
"""
|
|
179
|
+
Attach external splash controller. The splash will be closed
|
|
180
|
+
when the main window announces readiness.
|
|
181
|
+
"""
|
|
182
|
+
self._preloader = preloader
|
|
183
|
+
if self.window is not None:
|
|
184
|
+
try:
|
|
185
|
+
self.window.appReady.connect(self._on_window_ready, Qt.QueuedConnection)
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
def _on_window_ready(self):
|
|
190
|
+
"""
|
|
191
|
+
Close preloader when the first frame of the main window is ready.
|
|
192
|
+
"""
|
|
193
|
+
if self._preloader:
|
|
194
|
+
try:
|
|
195
|
+
self._preloader.close()
|
|
196
|
+
except Exception:
|
|
197
|
+
pass
|
|
198
|
+
self._preloader = None
|
|
139
199
|
|
|
140
200
|
def add_plugin(self, plugin: BasePlugin):
|
|
141
201
|
"""
|
|
@@ -290,4 +350,4 @@ class Launcher:
|
|
|
290
350
|
# self.window.core.debug.mem("INIT") # debug memory usage
|
|
291
351
|
signal.signal(signal.SIGTERM, self.handle_signal)
|
|
292
352
|
signal.signal(signal.SIGINT, self.handle_signal)
|
|
293
|
-
sys.exit(self.app.exec())
|
|
353
|
+
sys.exit(self.app.exec())
|
pygpt_net/preload.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# ================================================== #
|
|
4
|
+
# This file is a part of PYGPT package #
|
|
5
|
+
# Website: https://pygpt.net #
|
|
6
|
+
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
|
+
# MIT License #
|
|
8
|
+
# Created By : Marcin Szczygliński #
|
|
9
|
+
# Updated Date: 2025.12.31 17:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
# -------------------------------------------------- #
|
|
13
|
+
# Lightweight splash window (separate process)
|
|
14
|
+
# -------------------------------------------------- #
|
|
15
|
+
def _splash_main(conn, title="PyGPT", message="Loading…"):
|
|
16
|
+
"""
|
|
17
|
+
Minimal splash process using PySide6. Runs its own event loop and
|
|
18
|
+
listens for commands on a Pipe: {"type": "msg", "text": "..."} or {"type": "quit"}.
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
# Import locally to keep the main process import path untouched
|
|
22
|
+
from PySide6 import QtCore, QtWidgets
|
|
23
|
+
except Exception:
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
# Enable HiDPI (safe defaults)
|
|
28
|
+
try:
|
|
29
|
+
QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
|
|
30
|
+
QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
app = QtWidgets.QApplication(["pygpt_splash"])
|
|
35
|
+
|
|
36
|
+
# Root window styled as splash
|
|
37
|
+
root = QtWidgets.QWidget(
|
|
38
|
+
None,
|
|
39
|
+
QtCore.Qt.SplashScreen
|
|
40
|
+
| QtCore.Qt.FramelessWindowHint
|
|
41
|
+
| QtCore.Qt.WindowStaysOnTopHint
|
|
42
|
+
)
|
|
43
|
+
root.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
|
|
44
|
+
root.setObjectName("SplashRoot")
|
|
45
|
+
|
|
46
|
+
panel = QtWidgets.QFrame(root)
|
|
47
|
+
panel.setObjectName("SplashPanel")
|
|
48
|
+
panel.setStyleSheet("""
|
|
49
|
+
#SplashPanel {
|
|
50
|
+
background-color: rgba(30, 30, 30, 230);
|
|
51
|
+
border-radius: 12px;
|
|
52
|
+
}
|
|
53
|
+
QLabel { color: #ffffff; }
|
|
54
|
+
""")
|
|
55
|
+
layout = QtWidgets.QVBoxLayout(panel)
|
|
56
|
+
layout.setContentsMargins(16, 16, 16, 16)
|
|
57
|
+
layout.setSpacing(8)
|
|
58
|
+
|
|
59
|
+
lbl_title = QtWidgets.QLabel(title, panel)
|
|
60
|
+
lbl_title.setAlignment(QtCore.Qt.AlignCenter)
|
|
61
|
+
lbl_title.setStyleSheet("font-size: 16px; font-weight: 600;")
|
|
62
|
+
|
|
63
|
+
lbl_wait = QtWidgets.QLabel("Initializing...")
|
|
64
|
+
lbl_wait.setAlignment(QtCore.Qt.AlignCenter)
|
|
65
|
+
lbl_wait.setStyleSheet("font-size: 12px;")
|
|
66
|
+
|
|
67
|
+
lbl_msg = QtWidgets.QLabel(message, panel)
|
|
68
|
+
lbl_msg.setAlignment(QtCore.Qt.AlignCenter)
|
|
69
|
+
lbl_msg.setStyleSheet("font-size: 12px;")
|
|
70
|
+
|
|
71
|
+
bar = QtWidgets.QProgressBar(panel)
|
|
72
|
+
bar.setRange(0, 0)
|
|
73
|
+
bar.setTextVisible(False)
|
|
74
|
+
bar.setFixedHeight(8)
|
|
75
|
+
bar.setStyleSheet("QProgressBar { border: 0px; border-radius: 4px; } "
|
|
76
|
+
"QProgressBar::chunk { background-color: #3f3f3f; }")
|
|
77
|
+
|
|
78
|
+
layout.addWidget(lbl_title)
|
|
79
|
+
layout.addWidget(lbl_msg)
|
|
80
|
+
layout.addWidget(bar)
|
|
81
|
+
layout.addWidget(lbl_wait)
|
|
82
|
+
|
|
83
|
+
panel.setFixedSize(360, 120)
|
|
84
|
+
panel.move(0, 0)
|
|
85
|
+
root.resize(panel.size())
|
|
86
|
+
|
|
87
|
+
# Center on primary screen
|
|
88
|
+
screen = app.primaryScreen()
|
|
89
|
+
if screen:
|
|
90
|
+
geo = screen.availableGeometry()
|
|
91
|
+
root.move(geo.center() - root.rect().center())
|
|
92
|
+
|
|
93
|
+
# Ensure initial transparency for fade-in
|
|
94
|
+
try:
|
|
95
|
+
root.setWindowOpacity(0.0)
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
root.show()
|
|
100
|
+
|
|
101
|
+
# Fade-in on start (non-blocking, resilient to platform limitations)
|
|
102
|
+
try:
|
|
103
|
+
def _start_fade_in():
|
|
104
|
+
try:
|
|
105
|
+
anim = QtCore.QPropertyAnimation(root, b"windowOpacity")
|
|
106
|
+
anim.setDuration(300)
|
|
107
|
+
anim.setStartValue(0.0)
|
|
108
|
+
anim.setEndValue(1.0)
|
|
109
|
+
root._fade_in_anim = anim # keep reference to avoid GC
|
|
110
|
+
anim.start()
|
|
111
|
+
except Exception:
|
|
112
|
+
try:
|
|
113
|
+
root.setWindowOpacity(1.0)
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
QtCore.QTimer.singleShot(0, _start_fade_in)
|
|
118
|
+
except Exception:
|
|
119
|
+
try:
|
|
120
|
+
root.setWindowOpacity(1.0)
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
# Poll the pipe for messages and close requests
|
|
125
|
+
timer = QtCore.QTimer()
|
|
126
|
+
timer.setInterval(80)
|
|
127
|
+
|
|
128
|
+
def poll():
|
|
129
|
+
try:
|
|
130
|
+
if conn.poll():
|
|
131
|
+
msg = conn.recv()
|
|
132
|
+
if isinstance(msg, dict):
|
|
133
|
+
t = msg.get("type")
|
|
134
|
+
if t == "quit":
|
|
135
|
+
# Stop fade-in if it is running to avoid conflicting animations
|
|
136
|
+
try:
|
|
137
|
+
if hasattr(root, "_fade_in_anim") and root._fade_in_anim is not None:
|
|
138
|
+
root._fade_in_anim.stop()
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
# Fade-out and quit
|
|
142
|
+
try:
|
|
143
|
+
anim = QtCore.QPropertyAnimation(root, b"windowOpacity")
|
|
144
|
+
anim.setDuration(180)
|
|
145
|
+
anim.setStartValue(root.windowOpacity())
|
|
146
|
+
anim.setEndValue(0.0)
|
|
147
|
+
anim.finished.connect(app.quit)
|
|
148
|
+
# Keep reference to avoid GC
|
|
149
|
+
root._fade_anim = anim
|
|
150
|
+
anim.start()
|
|
151
|
+
except Exception:
|
|
152
|
+
app.quit()
|
|
153
|
+
elif t == "msg":
|
|
154
|
+
text = msg.get("text", "")
|
|
155
|
+
if text:
|
|
156
|
+
lbl_msg.setText(text)
|
|
157
|
+
elif isinstance(msg, str):
|
|
158
|
+
if msg.lower() == "quit":
|
|
159
|
+
app.quit()
|
|
160
|
+
except (EOFError, OSError):
|
|
161
|
+
# Parent died or pipe closed: exit
|
|
162
|
+
app.quit()
|
|
163
|
+
|
|
164
|
+
timer.timeout.connect(poll)
|
|
165
|
+
timer.start()
|
|
166
|
+
|
|
167
|
+
# Failsafe timeout (can be overridden via env)
|
|
168
|
+
import os as _os
|
|
169
|
+
timeout_ms = int(_os.environ.get("PYGPT_SPLASH_TIMEOUT", "120000"))
|
|
170
|
+
killer = QtCore.QTimer()
|
|
171
|
+
killer.setSingleShot(True)
|
|
172
|
+
killer.timeout.connect(app.quit)
|
|
173
|
+
killer.start(timeout_ms)
|
|
174
|
+
|
|
175
|
+
app.exec()
|
|
176
|
+
except Exception:
|
|
177
|
+
# No crash propagation to main app
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class _Preloader:
|
|
182
|
+
"""
|
|
183
|
+
Controller for the splash subprocess.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def __init__(self, proc, conn):
|
|
187
|
+
self._proc = proc
|
|
188
|
+
self._conn = conn
|
|
189
|
+
|
|
190
|
+
def set_message(self, text):
|
|
191
|
+
try:
|
|
192
|
+
if self._conn:
|
|
193
|
+
self._conn.send({"type": "msg", "text": str(text)})
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
def close(self, wait=True, timeout=2.0):
|
|
198
|
+
try:
|
|
199
|
+
if self._conn:
|
|
200
|
+
try:
|
|
201
|
+
self._conn.send({"type": "quit"})
|
|
202
|
+
except Exception:
|
|
203
|
+
pass
|
|
204
|
+
finally:
|
|
205
|
+
if wait and self._proc is not None:
|
|
206
|
+
self._proc.join(timeout=timeout)
|
|
207
|
+
if self._proc is not None and self._proc.is_alive():
|
|
208
|
+
try:
|
|
209
|
+
self._proc.terminate()
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
self._conn = None
|
|
213
|
+
self._proc = None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _start_preloader(title="PyGPT", message="Loading…"):
|
|
217
|
+
"""
|
|
218
|
+
Start splash as a separate process using 'spawn' on every OS.
|
|
219
|
+
Returns a _Preloader controller or None if failed.
|
|
220
|
+
"""
|
|
221
|
+
try:
|
|
222
|
+
import multiprocessing as mp
|
|
223
|
+
try:
|
|
224
|
+
ctx = mp.get_context("spawn")
|
|
225
|
+
except ValueError:
|
|
226
|
+
ctx = mp
|
|
227
|
+
|
|
228
|
+
parent_conn, child_conn = ctx.Pipe(duplex=True)
|
|
229
|
+
proc = ctx.Process(
|
|
230
|
+
target=_splash_main,
|
|
231
|
+
args=(child_conn, title, message),
|
|
232
|
+
daemon=True
|
|
233
|
+
)
|
|
234
|
+
proc.start()
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
child_conn.close()
|
|
238
|
+
except Exception:
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
return _Preloader(proc, parent_conn)
|
|
242
|
+
except Exception:
|
|
243
|
+
return None
|