pygpt-net 2.6.32__py3-none-any.whl → 2.6.33__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 +5 -0
- pygpt_net/__init__.py +1 -1
- pygpt_net/controller/chat/attachment.py +2 -0
- pygpt_net/controller/painter/common.py +10 -11
- pygpt_net/controller/painter/painter.py +4 -12
- pygpt_net/core/camera/camera.py +369 -53
- pygpt_net/data/config/config.json +233 -220
- pygpt_net/data/config/models.json +179 -180
- pygpt_net/data/locale/locale.de.ini +1 -0
- pygpt_net/data/locale/locale.en.ini +1 -0
- pygpt_net/data/locale/locale.es.ini +1 -0
- pygpt_net/data/locale/locale.fr.ini +1 -0
- pygpt_net/data/locale/locale.it.ini +1 -0
- pygpt_net/data/locale/locale.pl.ini +1 -0
- pygpt_net/data/locale/locale.uk.ini +1 -0
- pygpt_net/data/locale/locale.zh.ini +1 -0
- pygpt_net/ui/widget/draw/painter.py +452 -84
- {pygpt_net-2.6.32.dist-info → pygpt_net-2.6.33.dist-info}/METADATA +7 -2
- {pygpt_net-2.6.32.dist-info → pygpt_net-2.6.33.dist-info}/RECORD +22 -22
- {pygpt_net-2.6.32.dist-info → pygpt_net-2.6.33.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.32.dist-info → pygpt_net-2.6.33.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.32.dist-info → pygpt_net-2.6.33.dist-info}/entry_points.txt +0 -0
pygpt_net/CHANGELOG.txt
CHANGED
pygpt_net/__init__.py
CHANGED
|
@@ -13,7 +13,7 @@ __author__ = "Marcin Szczygliński"
|
|
|
13
13
|
__copyright__ = "Copyright 2025, Marcin Szczygliński"
|
|
14
14
|
__credits__ = ["Marcin Szczygliński"]
|
|
15
15
|
__license__ = "MIT"
|
|
16
|
-
__version__ = "2.6.
|
|
16
|
+
__version__ = "2.6.33"
|
|
17
17
|
__build__ = "2025-09-02"
|
|
18
18
|
__maintainer__ = "Marcin Szczygliński"
|
|
19
19
|
__github__ = "https://github.com/szczyglis-dev/py-gpt"
|
|
@@ -126,6 +126,8 @@ class Attachment(QObject):
|
|
|
126
126
|
self.uploaded = False
|
|
127
127
|
auto_index = self.window.core.config.get("attachments_auto_index", False)
|
|
128
128
|
attachments = self.window.core.attachments.get_all(mode, only_files=True)
|
|
129
|
+
if self.mode != self.MODE_QUERY_CONTEXT:
|
|
130
|
+
auto_index = False # disable auto index for full context and summary modes
|
|
129
131
|
|
|
130
132
|
if self.is_verbose() and len(attachments) > 0:
|
|
131
133
|
print(f"\nUploading attachments...\nWork Mode: {mode}")
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
|
|
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: 2024.12.14 08:00:00 #
|
|
10
|
-
# ================================================== #
|
|
1
|
+
# controller/painter/common.py
|
|
11
2
|
|
|
12
3
|
from typing import Tuple, Optional, Dict, List
|
|
13
4
|
|
|
@@ -49,8 +40,11 @@ class Common:
|
|
|
49
40
|
:param enabled: bool
|
|
50
41
|
"""
|
|
51
42
|
if enabled:
|
|
43
|
+
# keep UI color for compatibility
|
|
52
44
|
self.window.ui.nodes['painter.select.brush.color'].setCurrentText("Black")
|
|
53
45
|
self.window.ui.painter.set_brush_color(Qt.black)
|
|
46
|
+
# switch widget to brush mode (layered painting)
|
|
47
|
+
self.window.ui.painter.set_mode("brush")
|
|
54
48
|
self.window.core.config.set('painter.brush.mode', "brush")
|
|
55
49
|
self.window.core.config.save()
|
|
56
50
|
|
|
@@ -61,8 +55,11 @@ class Common:
|
|
|
61
55
|
:param enabled: bool
|
|
62
56
|
"""
|
|
63
57
|
if enabled:
|
|
58
|
+
# keep UI color for compatibility
|
|
64
59
|
self.window.ui.nodes['painter.select.brush.color'].setCurrentText("White")
|
|
65
60
|
self.window.ui.painter.set_brush_color(Qt.white)
|
|
61
|
+
# switch widget to erase mode (layered erasing)
|
|
62
|
+
self.window.ui.painter.set_mode("erase")
|
|
66
63
|
self.window.core.config.set('painter.brush.mode', "erase")
|
|
67
64
|
self.window.core.config.save()
|
|
68
65
|
|
|
@@ -76,8 +73,10 @@ class Common:
|
|
|
76
73
|
selected = self.window.ui.nodes['painter.select.canvas.size'].currentData()
|
|
77
74
|
if selected:
|
|
78
75
|
size = self.convert_to_size(selected)
|
|
76
|
+
# setCurrentText might not exist in the combo's items for custom sizes; harmless if it doesn't match
|
|
79
77
|
self.window.ui.nodes['painter.select.canvas.size'].setCurrentText(selected)
|
|
80
78
|
self.set_canvas_size(size[0], size[1])
|
|
79
|
+
# resizing the widget triggers automatic image rescale in PainterWidget.resizeEvent
|
|
81
80
|
self.window.core.config.set('painter.canvas.size', selected)
|
|
82
81
|
self.window.core.config.save()
|
|
83
82
|
|
|
@@ -192,4 +191,4 @@ class Common:
|
|
|
192
191
|
|
|
193
192
|
:return: path to capture directory
|
|
194
193
|
"""
|
|
195
|
-
return self.window.core.config.get_user_dir('capture')
|
|
194
|
+
return self.window.core.config.get_user_dir('capture')
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
|
|
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: 2024.11.20 21:00:00 #
|
|
10
|
-
# ================================================== #
|
|
1
|
+
# controller/painter/painter.py
|
|
11
2
|
|
|
12
3
|
import os
|
|
13
4
|
|
|
@@ -72,7 +63,8 @@ class Painter:
|
|
|
72
63
|
return
|
|
73
64
|
path = os.path.join(self.common.get_capture_dir(), '_current.png')
|
|
74
65
|
if os.path.exists(path):
|
|
75
|
-
|
|
66
|
+
# load as flat source; layers will be rebuilt on canvas resize
|
|
67
|
+
self.window.ui.painter.load_flat_image(path)
|
|
76
68
|
else:
|
|
77
69
|
# clear image
|
|
78
70
|
self.window.ui.painter.clear_image()
|
|
@@ -88,4 +80,4 @@ class Painter:
|
|
|
88
80
|
|
|
89
81
|
def reload(self):
|
|
90
82
|
"""Reload painter"""
|
|
91
|
-
self.setup()
|
|
83
|
+
self.setup()
|
pygpt_net/core/camera/camera.py
CHANGED
|
@@ -6,13 +6,14 @@
|
|
|
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.09.02 09:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import os
|
|
13
13
|
import time
|
|
14
14
|
|
|
15
|
-
from PySide6.QtCore import QObject, Signal, QRunnable, Slot
|
|
15
|
+
from PySide6.QtCore import QObject, Signal, QRunnable, Slot, QEventLoop, QTimer, Qt
|
|
16
|
+
from PySide6.QtGui import QImage
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class Camera:
|
|
@@ -50,85 +51,400 @@ class CaptureWorker(QRunnable):
|
|
|
50
51
|
self.args = args
|
|
51
52
|
self.kwargs = kwargs
|
|
52
53
|
self.window = None
|
|
54
|
+
|
|
55
|
+
# Common
|
|
53
56
|
self.initialized = False
|
|
54
|
-
self.capture = None
|
|
55
|
-
self.frame = None
|
|
56
57
|
self.allow_finish = False
|
|
58
|
+
self._fps_interval = 1.0 / 30.0 # default 30 FPS throttle
|
|
59
|
+
|
|
60
|
+
# Qt Multimedia objects (created in worker thread)
|
|
61
|
+
self.session = None
|
|
62
|
+
self.camera = None
|
|
63
|
+
self.sink = None
|
|
64
|
+
self.loop = None
|
|
65
|
+
self.poll_timer = None
|
|
66
|
+
self._qt_got_first_frame = False
|
|
67
|
+
self._probe_loop = None
|
|
68
|
+
|
|
69
|
+
# OpenCV fallback
|
|
70
|
+
self.cv_cap = None
|
|
71
|
+
|
|
72
|
+
# Timing (shared)
|
|
73
|
+
self._last_emit = 0.0
|
|
74
|
+
|
|
75
|
+
# =========================
|
|
76
|
+
# Qt Multimedia path
|
|
77
|
+
# =========================
|
|
78
|
+
def _select_camera_format(self, device, target_w: int, target_h: int):
|
|
79
|
+
"""
|
|
80
|
+
Select best matching camera format by resolution.
|
|
57
81
|
|
|
58
|
-
|
|
59
|
-
|
|
82
|
+
:param device: QCameraDevice
|
|
83
|
+
:param target_w: target width
|
|
84
|
+
:param target_h: target height
|
|
85
|
+
"""
|
|
60
86
|
try:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
87
|
+
formats = list(device.videoFormats())
|
|
88
|
+
except Exception:
|
|
89
|
+
formats = []
|
|
90
|
+
if not formats:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
best = None
|
|
94
|
+
best_score = float('inf')
|
|
95
|
+
for f in formats:
|
|
96
|
+
res = f.resolution()
|
|
97
|
+
w, h = res.width(), res.height()
|
|
98
|
+
score = abs(w - target_w) + abs(h - target_h)
|
|
99
|
+
if score < best_score:
|
|
100
|
+
best_score = score
|
|
101
|
+
best = f
|
|
102
|
+
return best
|
|
103
|
+
|
|
104
|
+
def _init_qt(self) -> bool:
|
|
105
|
+
"""
|
|
106
|
+
Try to initialize Qt camera pipeline.
|
|
107
|
+
|
|
108
|
+
:return: True if initialized
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
from PySide6.QtMultimedia import (
|
|
112
|
+
QCamera,
|
|
113
|
+
QMediaDevices,
|
|
114
|
+
QMediaCaptureSession,
|
|
115
|
+
QVideoSink,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
idx = int(self.window.core.config.get('vision.capture.idx'))
|
|
119
|
+
target_w = int(self.window.core.config.get('vision.capture.width'))
|
|
120
|
+
target_h = int(self.window.core.config.get('vision.capture.height'))
|
|
121
|
+
target_fps = 30
|
|
122
|
+
self._fps_interval = 1.0 / float(target_fps)
|
|
123
|
+
|
|
124
|
+
devices = list(QMediaDevices.videoInputs())
|
|
125
|
+
if not devices:
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
if idx < 0 or idx >= len(devices):
|
|
129
|
+
idx = 0
|
|
130
|
+
dev = devices[idx]
|
|
131
|
+
|
|
132
|
+
self.camera = QCamera(dev)
|
|
133
|
+
fmt = self._select_camera_format(dev, target_w, target_h)
|
|
134
|
+
if fmt is not None:
|
|
135
|
+
self.camera.setCameraFormat(fmt)
|
|
136
|
+
|
|
137
|
+
self.session = QMediaCaptureSession()
|
|
138
|
+
self.session.setCamera(self.camera)
|
|
139
|
+
|
|
140
|
+
self.sink = QVideoSink()
|
|
141
|
+
self.sink.videoFrameChanged.connect(self.on_qt_frame_changed, Qt.DirectConnection)
|
|
142
|
+
self.session.setVideoOutput(self.sink)
|
|
143
|
+
|
|
144
|
+
self.camera.errorOccurred.connect(self._on_qt_camera_error, Qt.QueuedConnection)
|
|
145
|
+
return True
|
|
146
|
+
|
|
70
147
|
except Exception as e:
|
|
148
|
+
# Qt Multimedia not available or failed to init
|
|
71
149
|
self.window.core.debug.log(e)
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
def _teardown_qt(self):
|
|
153
|
+
"""Release Qt camera pipeline."""
|
|
154
|
+
try:
|
|
155
|
+
if self.sink is not None:
|
|
156
|
+
try:
|
|
157
|
+
self.sink.videoFrameChanged.disconnect(self.on_qt_frame_changed)
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
if self.camera is not None and self.camera.isActive():
|
|
161
|
+
self.camera.stop()
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
finally:
|
|
165
|
+
self.sink = None
|
|
166
|
+
self.session = None
|
|
167
|
+
self.camera = None
|
|
168
|
+
|
|
169
|
+
def _probe_qt_start(self, timeout_ms: int = 1500) -> bool:
|
|
170
|
+
"""
|
|
171
|
+
Wait briefly for the first frame to confirm Qt pipeline is working.
|
|
172
|
+
|
|
173
|
+
:param timeout_ms: timeout in milliseconds
|
|
174
|
+
:return: True if first frame received
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
if self.camera is None:
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
self._qt_got_first_frame = False
|
|
181
|
+
self._probe_loop = QEventLoop()
|
|
182
|
+
|
|
183
|
+
# Timeout quits the probe loop
|
|
184
|
+
QTimer.singleShot(timeout_ms, self._probe_loop.quit)
|
|
185
|
+
|
|
186
|
+
# Start camera and wait for first frame or timeout
|
|
187
|
+
self.camera.start()
|
|
188
|
+
self._probe_loop.exec()
|
|
189
|
+
|
|
190
|
+
got = self._qt_got_first_frame
|
|
191
|
+
self._probe_loop = None
|
|
192
|
+
return got
|
|
193
|
+
except Exception as e:
|
|
194
|
+
self.window.core.debug.log(e)
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
@Slot(object)
|
|
198
|
+
def _on_qt_camera_error(self, err):
|
|
199
|
+
"""
|
|
200
|
+
Handle Qt camera errors.
|
|
201
|
+
|
|
202
|
+
:param err: error object
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
# Stop loop if running
|
|
206
|
+
if self.loop is not None and self.loop.isRunning():
|
|
207
|
+
self.loop.quit()
|
|
208
|
+
if self._probe_loop is not None and self._probe_loop.isRunning():
|
|
209
|
+
self._probe_loop.quit()
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
finally:
|
|
213
|
+
self.allow_finish = False
|
|
72
214
|
if self.signals is not None:
|
|
73
|
-
self.signals.error.emit(
|
|
74
|
-
|
|
215
|
+
self.signals.error.emit(err)
|
|
216
|
+
|
|
217
|
+
@Slot(object)
|
|
218
|
+
def on_qt_frame_changed(self, video_frame):
|
|
219
|
+
"""
|
|
220
|
+
Convert QVideoFrame to RGB numpy array and emit.
|
|
221
|
+
|
|
222
|
+
:param video_frame: QVideoFrame
|
|
223
|
+
"""
|
|
224
|
+
try:
|
|
225
|
+
# Mark that we have a first frame for probe
|
|
226
|
+
if not self._qt_got_first_frame:
|
|
227
|
+
self._qt_got_first_frame = True
|
|
228
|
+
# If we are probing, quit the probe loop immediately
|
|
229
|
+
if self._probe_loop is not None and self._probe_loop.isRunning():
|
|
230
|
+
self._probe_loop.quit()
|
|
231
|
+
|
|
232
|
+
# Throttle FPS for normal operation path
|
|
233
|
+
now = time.monotonic()
|
|
234
|
+
if self.loop is not None and self.loop.isRunning():
|
|
235
|
+
if (now - self._last_emit) < self._fps_interval:
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
img = video_frame.toImage()
|
|
239
|
+
if img.isNull():
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
img = img.convertToFormat(QImage.Format.Format_RGB888)
|
|
243
|
+
|
|
244
|
+
w = img.width()
|
|
245
|
+
h = img.height()
|
|
246
|
+
bpl = img.bytesPerLine()
|
|
75
247
|
|
|
248
|
+
ptr = img.bits()
|
|
249
|
+
size = bpl * h
|
|
250
|
+
try:
|
|
251
|
+
ptr.setsize(size)
|
|
252
|
+
except Exception:
|
|
253
|
+
# Some bindings may not require setsize; ignore if unsupported
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
import numpy as np
|
|
257
|
+
arr = np.frombuffer(ptr, dtype=np.uint8)
|
|
258
|
+
|
|
259
|
+
if bpl != w * 3:
|
|
260
|
+
arr = arr.reshape(h, bpl)[:, : w * 3]
|
|
261
|
+
arr = arr.reshape(h, w, 3).copy()
|
|
262
|
+
else:
|
|
263
|
+
arr = arr.reshape(h, w, 3).copy()
|
|
264
|
+
|
|
265
|
+
if self.signals is not None:
|
|
266
|
+
self.signals.capture.emit(arr)
|
|
267
|
+
self._last_emit = now
|
|
268
|
+
|
|
269
|
+
except Exception as e:
|
|
270
|
+
self.window.core.debug.log(e)
|
|
271
|
+
|
|
272
|
+
# =========================
|
|
273
|
+
# OpenCV fallback path
|
|
274
|
+
# =========================
|
|
275
|
+
def _init_cv2(self) -> bool:
|
|
276
|
+
"""
|
|
277
|
+
Try to initialize OpenCV VideoCapture fallback.
|
|
278
|
+
|
|
279
|
+
:return: True if initialized
|
|
280
|
+
"""
|
|
281
|
+
try:
|
|
282
|
+
import cv2
|
|
283
|
+
idx = int(self.window.core.config.get('vision.capture.idx'))
|
|
284
|
+
target_w = int(self.window.core.config.get('vision.capture.width'))
|
|
285
|
+
target_h = int(self.window.core.config.get('vision.capture.height'))
|
|
286
|
+
target_fps = 30
|
|
287
|
+
self._fps_interval = 1.0 / float(target_fps)
|
|
288
|
+
|
|
289
|
+
cap = cv2.VideoCapture(idx)
|
|
290
|
+
if not cap or not cap.isOpened():
|
|
291
|
+
return False
|
|
292
|
+
|
|
293
|
+
cap.set(cv2.CAP_PROP_FRAME_WIDTH, target_w)
|
|
294
|
+
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, target_h)
|
|
295
|
+
self.cv_cap = cap
|
|
296
|
+
return True
|
|
297
|
+
except Exception as e:
|
|
298
|
+
self.window.core.debug.log(e)
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
def _teardown_cv2(self):
|
|
302
|
+
"""Release OpenCV capture."""
|
|
303
|
+
try:
|
|
304
|
+
if self.cv_cap is not None and self.cv_cap.isOpened():
|
|
305
|
+
self.cv_cap.release()
|
|
306
|
+
except Exception:
|
|
307
|
+
pass
|
|
308
|
+
finally:
|
|
309
|
+
self.cv_cap = None
|
|
310
|
+
|
|
311
|
+
# =========================
|
|
312
|
+
# Runner
|
|
313
|
+
# =========================
|
|
76
314
|
@Slot()
|
|
77
315
|
def run(self):
|
|
78
|
-
"""
|
|
79
|
-
target_fps = 30
|
|
80
|
-
fps_interval = 1.0 / target_fps
|
|
316
|
+
"""Run capture using Qt first; fall back to OpenCV if needed."""
|
|
81
317
|
self.allow_finish = True
|
|
318
|
+
self._last_emit = 0.0
|
|
319
|
+
|
|
320
|
+
used_backend = None
|
|
82
321
|
try:
|
|
83
|
-
|
|
84
|
-
if
|
|
85
|
-
self.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
self.
|
|
95
|
-
self.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
322
|
+
# Try Qt Multimedia
|
|
323
|
+
if self._init_qt():
|
|
324
|
+
if self._probe_qt_start(timeout_ms=1500):
|
|
325
|
+
# Qt confirmed working; start main event-driven loop
|
|
326
|
+
used_backend = 'qt'
|
|
327
|
+
self.initialized = True
|
|
328
|
+
if self.signals is not None:
|
|
329
|
+
self.signals.started.emit()
|
|
330
|
+
|
|
331
|
+
self.loop = QEventLoop()
|
|
332
|
+
|
|
333
|
+
self.poll_timer = QTimer()
|
|
334
|
+
self.poll_timer.setTimerType(Qt.PreciseTimer)
|
|
335
|
+
self.poll_timer.setInterval(30)
|
|
336
|
+
self.poll_timer.timeout.connect(self._poll_stop_qt, Qt.DirectConnection)
|
|
337
|
+
self.poll_timer.start()
|
|
338
|
+
|
|
339
|
+
self.loop.exec()
|
|
340
|
+
|
|
341
|
+
if self.signals is not None:
|
|
342
|
+
self.signals.stopped.emit()
|
|
343
|
+
else:
|
|
344
|
+
# Fallback to OpenCV if no frames arrive quickly
|
|
345
|
+
print("QT camera init failed, trying CV2 fallback...")
|
|
346
|
+
self._teardown_qt()
|
|
347
|
+
else:
|
|
348
|
+
# Qt init failed outright, fallback to CV2
|
|
349
|
+
print("QT camera init failed, trying CV2 fallback...")
|
|
350
|
+
|
|
351
|
+
# Try OpenCV fallback if Qt was not used
|
|
352
|
+
if used_backend is None:
|
|
353
|
+
if self._init_cv2():
|
|
354
|
+
used_backend = 'cv2'
|
|
355
|
+
self.initialized = True
|
|
356
|
+
if self.signals is not None:
|
|
357
|
+
self.signals.started.emit()
|
|
358
|
+
|
|
359
|
+
import cv2
|
|
360
|
+
target_fps = 30
|
|
361
|
+
fps_interval = 1.0 / float(target_fps)
|
|
362
|
+
last_frame_time = time.time()
|
|
363
|
+
|
|
364
|
+
while True:
|
|
365
|
+
if self._should_stop():
|
|
366
|
+
break
|
|
367
|
+
|
|
368
|
+
ok, frame = self.cv_cap.read()
|
|
369
|
+
if not ok or frame is None:
|
|
370
|
+
continue
|
|
371
|
+
|
|
372
|
+
now = time.time()
|
|
373
|
+
if now - last_frame_time >= fps_interval:
|
|
374
|
+
# Convert BGR -> RGB for the controller/UI pipeline
|
|
375
|
+
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
376
|
+
if self.signals is not None:
|
|
377
|
+
self.signals.capture.emit(frame)
|
|
378
|
+
last_frame_time = now
|
|
379
|
+
|
|
380
|
+
if self.signals is not None:
|
|
381
|
+
self.signals.stopped.emit()
|
|
382
|
+
else:
|
|
383
|
+
# Both providers failed
|
|
384
|
+
self.allow_finish = False
|
|
103
385
|
|
|
104
386
|
except Exception as e:
|
|
105
387
|
self.window.core.debug.log(e)
|
|
106
388
|
if self.signals is not None:
|
|
107
389
|
self.signals.error.emit(e)
|
|
108
|
-
|
|
109
390
|
finally:
|
|
110
|
-
|
|
391
|
+
# Cleanup resources
|
|
392
|
+
try:
|
|
393
|
+
if self.poll_timer is not None:
|
|
394
|
+
self.poll_timer.stop()
|
|
395
|
+
except Exception:
|
|
396
|
+
pass
|
|
397
|
+
self.poll_timer = None
|
|
398
|
+
|
|
399
|
+
if used_backend == 'qt':
|
|
400
|
+
self._teardown_qt()
|
|
401
|
+
else:
|
|
402
|
+
self._teardown_qt() # no-op if not initialized
|
|
403
|
+
self._teardown_cv2()
|
|
404
|
+
|
|
405
|
+
# Emit final state
|
|
111
406
|
if self.signals is not None:
|
|
112
407
|
if self.allow_finish:
|
|
113
408
|
self.signals.finished.emit()
|
|
114
409
|
else:
|
|
115
410
|
self.signals.unfinished.emit()
|
|
411
|
+
|
|
116
412
|
self.cleanup()
|
|
117
413
|
|
|
118
|
-
def
|
|
119
|
-
"""
|
|
120
|
-
|
|
121
|
-
self.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
414
|
+
def _poll_stop_qt(self):
|
|
415
|
+
"""Check stop flags while running Qt pipeline."""
|
|
416
|
+
try:
|
|
417
|
+
if self._should_stop():
|
|
418
|
+
if self.camera is not None and self.camera.isActive():
|
|
419
|
+
self.camera.stop()
|
|
420
|
+
if self.loop is not None and self.loop.isRunning():
|
|
421
|
+
self.loop.quit()
|
|
422
|
+
except Exception as e:
|
|
423
|
+
self.window.core.debug.log(e)
|
|
424
|
+
if self.loop is not None and self.loop.isRunning():
|
|
425
|
+
self.loop.quit()
|
|
426
|
+
|
|
427
|
+
def _should_stop(self) -> bool:
|
|
428
|
+
"""
|
|
429
|
+
Check external stop flags.
|
|
430
|
+
|
|
431
|
+
:return: True if should stop
|
|
432
|
+
"""
|
|
433
|
+
try:
|
|
434
|
+
if getattr(self.window, 'is_closing', False):
|
|
435
|
+
return True
|
|
436
|
+
if self.window is not None and self.window.controller.camera.stop:
|
|
437
|
+
return True
|
|
438
|
+
except Exception:
|
|
439
|
+
return True
|
|
440
|
+
return False
|
|
125
441
|
|
|
126
442
|
def cleanup(self):
|
|
127
443
|
"""Cleanup resources after worker execution."""
|
|
128
444
|
sig = self.signals
|
|
129
445
|
self.signals = None
|
|
130
|
-
|
|
131
|
-
|
|
446
|
+
try:
|
|
447
|
+
if sig is not None:
|
|
132
448
|
sig.deleteLater()
|
|
133
|
-
|
|
134
|
-
|
|
449
|
+
except RuntimeError:
|
|
450
|
+
pass
|