pygpt-net 2.6.32__py3-none-any.whl → 2.6.34__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/controller/assistant/batch.py +14 -4
- pygpt_net/controller/assistant/files.py +1 -0
- pygpt_net/controller/assistant/store.py +195 -1
- pygpt_net/controller/camera/camera.py +1 -1
- pygpt_net/controller/chat/attachment.py +2 -0
- pygpt_net/controller/chat/common.py +50 -46
- pygpt_net/controller/config/placeholder.py +95 -75
- pygpt_net/controller/dialogs/confirm.py +3 -1
- pygpt_net/controller/media/media.py +11 -3
- pygpt_net/controller/painter/common.py +227 -10
- pygpt_net/controller/painter/painter.py +4 -12
- pygpt_net/core/assistants/files.py +18 -0
- pygpt_net/core/camera/camera.py +38 -93
- pygpt_net/core/camera/worker.py +430 -0
- pygpt_net/core/filesystem/url.py +3 -0
- pygpt_net/core/render/web/body.py +65 -9
- pygpt_net/core/text/utils.py +3 -0
- pygpt_net/data/config/config.json +234 -221
- pygpt_net/data/config/models.json +179 -180
- pygpt_net/data/config/settings.json +10 -5
- pygpt_net/data/locale/locale.de.ini +8 -6
- pygpt_net/data/locale/locale.en.ini +9 -5
- pygpt_net/data/locale/locale.es.ini +8 -6
- pygpt_net/data/locale/locale.fr.ini +8 -6
- pygpt_net/data/locale/locale.it.ini +8 -6
- pygpt_net/data/locale/locale.pl.ini +8 -6
- pygpt_net/data/locale/locale.uk.ini +8 -6
- pygpt_net/data/locale/locale.zh.ini +8 -6
- pygpt_net/item/assistant.py +13 -1
- pygpt_net/provider/api/google/__init__.py +32 -23
- pygpt_net/provider/api/openai/store.py +45 -1
- pygpt_net/provider/llms/google.py +4 -0
- pygpt_net/ui/dialog/assistant_store.py +213 -203
- pygpt_net/ui/layout/chat/input.py +3 -3
- pygpt_net/ui/widget/draw/painter.py +458 -75
- pygpt_net/ui/widget/option/combo.py +5 -1
- pygpt_net/ui/widget/textarea/input.py +273 -3
- {pygpt_net-2.6.32.dist-info → pygpt_net-2.6.34.dist-info}/METADATA +14 -2
- {pygpt_net-2.6.32.dist-info → pygpt_net-2.6.34.dist-info}/RECORD +44 -43
- {pygpt_net-2.6.32.dist-info → pygpt_net-2.6.34.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.32.dist-info → pygpt_net-2.6.34.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.32.dist-info → pygpt_net-2.6.34.dist-info}/entry_points.txt +0 -0
|
@@ -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:
|
|
9
|
+
# Updated Date: 2025.09.02 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from typing import Tuple, Optional, Dict, List
|
|
13
13
|
|
|
14
14
|
from PySide6.QtCore import Qt, QSize
|
|
15
15
|
from PySide6.QtGui import QColor
|
|
16
|
+
from PySide6.QtWidgets import QComboBox
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class Common:
|
|
@@ -23,6 +24,12 @@ class Common:
|
|
|
23
24
|
:param window: Window instance
|
|
24
25
|
"""
|
|
25
26
|
self.window = window
|
|
27
|
+
# Guard to prevent re-entrancy when programmatically changing the combo/size
|
|
28
|
+
self._changing_canvas_size = False
|
|
29
|
+
# Cached set for predefined canvas sizes
|
|
30
|
+
self._predef_canvas_sizes_set = None
|
|
31
|
+
# Sticky custom value derived from the current "source" image (kept at index 0 when present)
|
|
32
|
+
self._sticky_custom_value: Optional[str] = None
|
|
26
33
|
|
|
27
34
|
def convert_to_size(self, canvas_size: str) -> Tuple[int, int]:
|
|
28
35
|
"""
|
|
@@ -37,8 +44,8 @@ class Common:
|
|
|
37
44
|
"""
|
|
38
45
|
Set canvas size
|
|
39
46
|
|
|
40
|
-
:param width:
|
|
41
|
-
:param height:
|
|
47
|
+
:param width: Canvas width
|
|
48
|
+
:param height: Canvas height
|
|
42
49
|
"""
|
|
43
50
|
self.window.ui.painter.setFixedSize(QSize(width, height))
|
|
44
51
|
|
|
@@ -49,8 +56,11 @@ class Common:
|
|
|
49
56
|
:param enabled: bool
|
|
50
57
|
"""
|
|
51
58
|
if enabled:
|
|
59
|
+
# keep UI color for compatibility
|
|
52
60
|
self.window.ui.nodes['painter.select.brush.color'].setCurrentText("Black")
|
|
53
61
|
self.window.ui.painter.set_brush_color(Qt.black)
|
|
62
|
+
# switch widget to brush mode (layered painting)
|
|
63
|
+
self.window.ui.painter.set_mode("brush")
|
|
54
64
|
self.window.core.config.set('painter.brush.mode', "brush")
|
|
55
65
|
self.window.core.config.save()
|
|
56
66
|
|
|
@@ -61,8 +71,11 @@ class Common:
|
|
|
61
71
|
:param enabled: bool
|
|
62
72
|
"""
|
|
63
73
|
if enabled:
|
|
74
|
+
# keep UI color for compatibility
|
|
64
75
|
self.window.ui.nodes['painter.select.brush.color'].setCurrentText("White")
|
|
65
76
|
self.window.ui.painter.set_brush_color(Qt.white)
|
|
77
|
+
# switch widget to erase mode (layered erasing)
|
|
78
|
+
self.window.ui.painter.set_mode("erase")
|
|
66
79
|
self.window.core.config.set('painter.brush.mode', "erase")
|
|
67
80
|
self.window.core.config.save()
|
|
68
81
|
|
|
@@ -72,14 +85,64 @@ class Common:
|
|
|
72
85
|
|
|
73
86
|
:param selected: Selected size
|
|
74
87
|
"""
|
|
88
|
+
# Re-entrancy guard to avoid loops when we adjust the combo programmatically
|
|
89
|
+
if self._changing_canvas_size:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
combo: QComboBox = self.window.ui.nodes['painter.select.canvas.size']
|
|
93
|
+
painter = self.window.ui.painter
|
|
94
|
+
|
|
95
|
+
# Heuristic to detect manual UI change vs programmatic call
|
|
96
|
+
# - manual if: no arg, or int index (Qt int overload), or arg equals currentText/currentData
|
|
97
|
+
raw_arg = selected
|
|
98
|
+
current_text = combo.currentText()
|
|
99
|
+
current_data = combo.currentData()
|
|
100
|
+
current_data_str = current_data if isinstance(current_data, str) else None
|
|
101
|
+
is_manual = (
|
|
102
|
+
raw_arg is None
|
|
103
|
+
or isinstance(raw_arg, int)
|
|
104
|
+
or (isinstance(raw_arg, str) and (raw_arg == current_text or (current_data_str and raw_arg == current_data_str)))
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Resolve selection if not passed explicitly; fallback to currentText if userData is missing
|
|
75
108
|
if not selected:
|
|
76
|
-
selected =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
109
|
+
selected = current_data_str or current_text
|
|
110
|
+
|
|
111
|
+
# Normalize to "WxH" strictly; if invalid, do nothing
|
|
112
|
+
selected_norm = self._normalize_canvas_value(selected)
|
|
113
|
+
if not selected_norm:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# Save undo only for manual changes and only if size will change
|
|
117
|
+
will_change = selected_norm != f"{painter.width()}x{painter.height()}"
|
|
118
|
+
if is_manual and will_change:
|
|
119
|
+
painter.saveForUndo()
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
self._changing_canvas_size = True
|
|
123
|
+
|
|
124
|
+
predef = self._get_predef_canvas_set()
|
|
125
|
+
|
|
126
|
+
# Sticky custom update only for programmatic (source-driven) changes
|
|
127
|
+
programmatic = not is_manual
|
|
128
|
+
if programmatic:
|
|
129
|
+
if selected_norm in predef:
|
|
130
|
+
self._sticky_custom_value = None
|
|
131
|
+
else:
|
|
132
|
+
self._sticky_custom_value = selected_norm
|
|
133
|
+
|
|
134
|
+
# Ensure combo reflects single custom at index 0 (sticky respected), then select current value
|
|
135
|
+
self._sync_canvas_size_combo(combo, selected_norm, sticky_to_keep=self._sticky_custom_value)
|
|
136
|
+
|
|
137
|
+
# Apply canvas size; PainterWidget handles rescaling in resizeEvent
|
|
138
|
+
w, h = self.convert_to_size(selected_norm)
|
|
139
|
+
self.set_canvas_size(w, h)
|
|
140
|
+
|
|
141
|
+
# Persist normalized value
|
|
142
|
+
self.window.core.config.set('painter.canvas.size', selected_norm)
|
|
82
143
|
self.window.core.config.save()
|
|
144
|
+
finally:
|
|
145
|
+
self._changing_canvas_size = False
|
|
83
146
|
|
|
84
147
|
def change_brush_size(self, size: int):
|
|
85
148
|
"""
|
|
@@ -142,7 +205,7 @@ class Common:
|
|
|
142
205
|
if self.window.core.config.has('painter.brush.size'):
|
|
143
206
|
size = int(self.window.core.config.get('painter.brush.size', 3))
|
|
144
207
|
self.window.ui.nodes['painter.select.brush.size'].setCurrentIndex(
|
|
145
|
-
|
|
208
|
+
self.window.ui.nodes['painter.select.brush.size'].findText(str(size))
|
|
146
209
|
)
|
|
147
210
|
|
|
148
211
|
def get_colors(self) -> Dict[str, QColor]:
|
|
@@ -193,3 +256,157 @@ class Common:
|
|
|
193
256
|
:return: path to capture directory
|
|
194
257
|
"""
|
|
195
258
|
return self.window.core.config.get_user_dir('capture')
|
|
259
|
+
|
|
260
|
+
# ---------- Public sync helper (used by PainterWidget undo/redo) ----------
|
|
261
|
+
|
|
262
|
+
def sync_canvas_combo_from_widget(self):
|
|
263
|
+
"""
|
|
264
|
+
Sync the size combobox with current PainterWidget canvas size.
|
|
265
|
+
Also derive sticky custom from the current source image if it is custom.
|
|
266
|
+
This method does not change the canvas size (UI-only sync).
|
|
267
|
+
"""
|
|
268
|
+
if self._changing_canvas_size:
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
combo: QComboBox = self.window.ui.nodes['painter.select.canvas.size']
|
|
272
|
+
painter = self.window.ui.painter
|
|
273
|
+
|
|
274
|
+
canvas_value = f"{painter.width()}x{painter.height()}"
|
|
275
|
+
canvas_norm = self._normalize_canvas_value(canvas_value)
|
|
276
|
+
if not canvas_norm:
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
# Derive sticky from current source image (if custom)
|
|
280
|
+
predef = self._get_predef_canvas_set()
|
|
281
|
+
sticky = None
|
|
282
|
+
if painter.sourceImageOriginal is not None and not painter.sourceImageOriginal.isNull():
|
|
283
|
+
src_val = f"{painter.sourceImageOriginal.width()}x{painter.sourceImageOriginal.height()}"
|
|
284
|
+
src_val = self._normalize_canvas_value(src_val)
|
|
285
|
+
if src_val and src_val not in predef:
|
|
286
|
+
sticky = src_val
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
self._changing_canvas_size = True
|
|
290
|
+
self._sticky_custom_value = sticky
|
|
291
|
+
self._sync_canvas_size_combo(combo, canvas_norm, sticky_to_keep=sticky)
|
|
292
|
+
|
|
293
|
+
# Persist canvas size only (do not change sticky config-scope)
|
|
294
|
+
self.window.core.config.set('painter.canvas.size', canvas_norm)
|
|
295
|
+
self.window.core.config.save()
|
|
296
|
+
finally:
|
|
297
|
+
self._changing_canvas_size = False
|
|
298
|
+
|
|
299
|
+
# ---------- Internal helpers ----------
|
|
300
|
+
|
|
301
|
+
def _normalize_canvas_value(self, value: Optional[str]) -> Optional[str]:
|
|
302
|
+
"""
|
|
303
|
+
Normalize arbitrary canvas string to canonical 'WxH'. Returns None if invalid.
|
|
304
|
+
Accepts variants like ' 1024 x 768 ', '1024×768', etc.
|
|
305
|
+
|
|
306
|
+
:param value: input value
|
|
307
|
+
:return: normalized value or None
|
|
308
|
+
"""
|
|
309
|
+
if not value:
|
|
310
|
+
return None
|
|
311
|
+
s = str(value).strip().lower().replace(' ', '').replace('×', 'x')
|
|
312
|
+
if 'x' not in s:
|
|
313
|
+
return None
|
|
314
|
+
parts = s.split('x', 1)
|
|
315
|
+
try:
|
|
316
|
+
w = int(parts[0])
|
|
317
|
+
h = int(parts[1])
|
|
318
|
+
except Exception:
|
|
319
|
+
return None
|
|
320
|
+
if w <= 0 or h <= 0:
|
|
321
|
+
return None
|
|
322
|
+
return f"{w}x{h}"
|
|
323
|
+
|
|
324
|
+
def _get_predef_canvas_set(self) -> set:
|
|
325
|
+
"""
|
|
326
|
+
Return cached set of predefined sizes for O(1) lookups.
|
|
327
|
+
|
|
328
|
+
:return: set of predefined sizes
|
|
329
|
+
"""
|
|
330
|
+
if self._predef_canvas_sizes_set is None:
|
|
331
|
+
self._predef_canvas_sizes_set = set(self.get_canvas_sizes())
|
|
332
|
+
return self._predef_canvas_sizes_set
|
|
333
|
+
|
|
334
|
+
def _find_index_for_value(self, combo: QComboBox, value: str) -> int:
|
|
335
|
+
"""
|
|
336
|
+
Find index by userData first, then by text. Returns -1 if not found.
|
|
337
|
+
|
|
338
|
+
:param combo: QComboBox
|
|
339
|
+
:param value: value to find
|
|
340
|
+
:return: index or -1
|
|
341
|
+
"""
|
|
342
|
+
idx = combo.findData(value)
|
|
343
|
+
if idx == -1:
|
|
344
|
+
idx = combo.findText(value, Qt.MatchFixedString)
|
|
345
|
+
return idx
|
|
346
|
+
|
|
347
|
+
def _remove_extra_custom_items(self, combo: QComboBox, predef: set, keep_index: int = -1):
|
|
348
|
+
"""
|
|
349
|
+
Remove all non-predefined items except one at keep_index (if set).
|
|
350
|
+
|
|
351
|
+
:param combo: QComboBox
|
|
352
|
+
:param predef: set of predefined values
|
|
353
|
+
:param keep_index: index to keep even if custom, or -1 to remove all custom
|
|
354
|
+
"""
|
|
355
|
+
for i in range(combo.count() - 1, -1, -1):
|
|
356
|
+
if i == keep_index:
|
|
357
|
+
continue
|
|
358
|
+
txt = combo.itemText(i)
|
|
359
|
+
if txt not in predef:
|
|
360
|
+
combo.removeItem(i)
|
|
361
|
+
|
|
362
|
+
def _ensure_custom_index0(self, combo: QComboBox, custom_value: str, predef: set):
|
|
363
|
+
"""
|
|
364
|
+
Ensure exactly one custom item exists at index 0 with given value.
|
|
365
|
+
|
|
366
|
+
:param combo: QComboBox
|
|
367
|
+
:param custom_value: custom value to set at index 0
|
|
368
|
+
:param predef: set of predefined values
|
|
369
|
+
"""
|
|
370
|
+
if combo.count() > 0 and combo.itemText(0) not in predef:
|
|
371
|
+
if combo.itemText(0) != custom_value:
|
|
372
|
+
combo.setItemText(0, custom_value)
|
|
373
|
+
combo.setItemData(0, custom_value)
|
|
374
|
+
else:
|
|
375
|
+
combo.insertItem(0, custom_value, custom_value)
|
|
376
|
+
self._remove_extra_custom_items(combo, predef, keep_index=0)
|
|
377
|
+
|
|
378
|
+
def _sync_canvas_size_combo(self, combo: QComboBox, value: str, sticky_to_keep: Optional[str]):
|
|
379
|
+
"""
|
|
380
|
+
Enforce invariant and selection:
|
|
381
|
+
- If sticky_to_keep is a custom value -> keep it as single custom item at index 0.
|
|
382
|
+
- If sticky_to_keep is None -> remove all custom items.
|
|
383
|
+
- Select 'value' in the combo. If value is custom and sticky_to_keep differs or is None,
|
|
384
|
+
ensure index 0 matches 'value' and select it.
|
|
385
|
+
|
|
386
|
+
:param combo: QComboBox
|
|
387
|
+
:param value: current canvas size value to select
|
|
388
|
+
:param sticky_to_keep: sticky custom value to keep at index 0, or None
|
|
389
|
+
"""
|
|
390
|
+
predef = self._get_predef_canvas_set()
|
|
391
|
+
|
|
392
|
+
# Maintain sticky custom slot (index 0) if provided
|
|
393
|
+
if sticky_to_keep and sticky_to_keep not in predef:
|
|
394
|
+
self._ensure_custom_index0(combo, sticky_to_keep, predef)
|
|
395
|
+
else:
|
|
396
|
+
self._remove_extra_custom_items(combo, predef, keep_index=-1)
|
|
397
|
+
|
|
398
|
+
# Select the current canvas value
|
|
399
|
+
if value in predef:
|
|
400
|
+
idx = self._find_index_for_value(combo, value)
|
|
401
|
+
if idx != -1 and idx != combo.currentIndex():
|
|
402
|
+
combo.setCurrentIndex(idx)
|
|
403
|
+
elif idx == -1:
|
|
404
|
+
# Fallback: set text (should not normally happen if combo prepopulates predefined sizes)
|
|
405
|
+
combo.setCurrentText(value)
|
|
406
|
+
else:
|
|
407
|
+
# Current value is custom: ensure it exists at index 0 and select it
|
|
408
|
+
# If sticky differs or is None, overwrite/create the custom at index 0 to reflect true current value.
|
|
409
|
+
if not sticky_to_keep or sticky_to_keep != value:
|
|
410
|
+
self._ensure_custom_index0(combo, value, predef)
|
|
411
|
+
if combo.currentIndex() != 0:
|
|
412
|
+
combo.setCurrentIndex(0)
|
|
@@ -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()
|
|
@@ -257,6 +257,24 @@ class Files:
|
|
|
257
257
|
del self.items[file.record_id]
|
|
258
258
|
return True
|
|
259
259
|
|
|
260
|
+
def delete_by_file_id(self, file_id: str) -> bool:
|
|
261
|
+
"""
|
|
262
|
+
Delete file by file ID
|
|
263
|
+
|
|
264
|
+
:param file_id: file ID
|
|
265
|
+
:return: True if deleted
|
|
266
|
+
"""
|
|
267
|
+
res = self.provider.delete_by_file_id(file_id)
|
|
268
|
+
if res:
|
|
269
|
+
# remove from items
|
|
270
|
+
to_delete = []
|
|
271
|
+
for id in self.items:
|
|
272
|
+
if self.items[id].file_id == file_id:
|
|
273
|
+
to_delete.append(id)
|
|
274
|
+
for id in to_delete:
|
|
275
|
+
del self.items[id]
|
|
276
|
+
return res
|
|
277
|
+
|
|
260
278
|
def on_store_deleted(self, store_id: str):
|
|
261
279
|
"""
|
|
262
280
|
Clear deleted store from files
|
pygpt_net/core/camera/camera.py
CHANGED
|
@@ -6,13 +6,11 @@
|
|
|
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 16:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import os
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
from PySide6.QtCore import QObject, Signal, QRunnable, Slot
|
|
13
|
+
from typing import List
|
|
16
14
|
|
|
17
15
|
|
|
18
16
|
class Camera:
|
|
@@ -32,103 +30,50 @@ class Camera:
|
|
|
32
30
|
if not os.path.exists(img_dir):
|
|
33
31
|
os.makedirs(img_dir, exist_ok=True)
|
|
34
32
|
|
|
33
|
+
def get_devices_data(self) -> List[dict]:
|
|
34
|
+
"""
|
|
35
|
+
Return a list of camera devices for UI selection.
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
stopped = Signal()
|
|
42
|
-
capture = Signal(object)
|
|
43
|
-
error = Signal(object)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
class CaptureWorker(QRunnable):
|
|
47
|
-
def __init__(self, *args, **kwargs):
|
|
48
|
-
super().__init__()
|
|
49
|
-
self.signals = CaptureSignals()
|
|
50
|
-
self.args = args
|
|
51
|
-
self.kwargs = kwargs
|
|
52
|
-
self.window = None
|
|
53
|
-
self.initialized = False
|
|
54
|
-
self.capture = None
|
|
55
|
-
self.frame = None
|
|
56
|
-
self.allow_finish = False
|
|
37
|
+
Format:
|
|
38
|
+
[
|
|
39
|
+
{'id': <int index>, 'name': <str description>},
|
|
40
|
+
...
|
|
41
|
+
]
|
|
57
42
|
|
|
58
|
-
|
|
59
|
-
"""
|
|
43
|
+
'id' is the ordinal index used by vision.capture.idx.
|
|
44
|
+
"""
|
|
60
45
|
try:
|
|
61
|
-
import
|
|
62
|
-
# get params from global config
|
|
63
|
-
self.capture = cv2.VideoCapture(self.window.core.config.get('vision.capture.idx'))
|
|
64
|
-
if not self.capture or not self.capture.isOpened():
|
|
65
|
-
self.allow_finish = False
|
|
66
|
-
self.signals.unfinished.emit()
|
|
67
|
-
return
|
|
68
|
-
self.capture.set(cv2.CAP_PROP_FRAME_WIDTH, self.window.core.config.get('vision.capture.width'))
|
|
69
|
-
self.capture.set(cv2.CAP_PROP_FRAME_HEIGHT, self.window.core.config.get('vision.capture.height'))
|
|
46
|
+
from PySide6.QtMultimedia import QMediaDevices
|
|
70
47
|
except Exception as e:
|
|
48
|
+
# Qt Multimedia not available
|
|
71
49
|
self.window.core.debug.log(e)
|
|
72
|
-
|
|
73
|
-
self.signals.error.emit(e)
|
|
74
|
-
self.signals.finished.emit(e)
|
|
50
|
+
return []
|
|
75
51
|
|
|
76
|
-
@Slot()
|
|
77
|
-
def run(self):
|
|
78
|
-
"""Frame capture loop"""
|
|
79
|
-
target_fps = 30
|
|
80
|
-
fps_interval = 1.0 / target_fps
|
|
81
|
-
self.allow_finish = True
|
|
82
52
|
try:
|
|
83
|
-
|
|
84
|
-
if not self.initialized:
|
|
85
|
-
self.setup_camera()
|
|
86
|
-
self.signals.started.emit()
|
|
87
|
-
self.initialized = True
|
|
88
|
-
last_frame_time = time.time()
|
|
89
|
-
while True:
|
|
90
|
-
if self.window.is_closing \
|
|
91
|
-
or self.capture is None \
|
|
92
|
-
or not self.capture.isOpened() \
|
|
93
|
-
or self.window.controller.camera.stop:
|
|
94
|
-
self.release() # release camera
|
|
95
|
-
self.signals.stopped.emit()
|
|
96
|
-
break
|
|
97
|
-
_, frame = self.capture.read()
|
|
98
|
-
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
99
|
-
now = time.time()
|
|
100
|
-
if now - last_frame_time >= fps_interval:
|
|
101
|
-
self.signals.capture.emit(frame)
|
|
102
|
-
last_frame_time = now
|
|
103
|
-
|
|
53
|
+
devices = list(QMediaDevices.videoInputs())
|
|
104
54
|
except Exception as e:
|
|
105
55
|
self.window.core.debug.log(e)
|
|
106
|
-
|
|
107
|
-
self.signals.error.emit(e)
|
|
108
|
-
|
|
109
|
-
finally:
|
|
110
|
-
self.release() # release camera
|
|
111
|
-
if self.signals is not None:
|
|
112
|
-
if self.allow_finish:
|
|
113
|
-
self.signals.finished.emit()
|
|
114
|
-
else:
|
|
115
|
-
self.signals.unfinished.emit()
|
|
116
|
-
self.cleanup()
|
|
117
|
-
|
|
118
|
-
def release(self):
|
|
119
|
-
"""Release camera"""
|
|
120
|
-
if self.capture is not None and self.capture.isOpened():
|
|
121
|
-
self.capture.release()
|
|
122
|
-
self.capture = None
|
|
123
|
-
self.frame = None
|
|
124
|
-
self.initialized = False
|
|
56
|
+
return []
|
|
125
57
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
sig = self.signals
|
|
129
|
-
self.signals = None
|
|
130
|
-
if sig is not None:
|
|
58
|
+
result = []
|
|
59
|
+
for idx, dev in enumerate(devices):
|
|
131
60
|
try:
|
|
132
|
-
|
|
133
|
-
except
|
|
134
|
-
|
|
61
|
+
name = dev.description()
|
|
62
|
+
except Exception:
|
|
63
|
+
name = f"Camera {idx}"
|
|
64
|
+
result.append({'id': idx, 'name': name})
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
def get_devices(self) -> List[dict]:
|
|
68
|
+
"""
|
|
69
|
+
Get choices list of single-pair dicts {id: name}.
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
[
|
|
73
|
+
{'0': 'Integrated Camera'},
|
|
74
|
+
{'1': 'USB Camera'},
|
|
75
|
+
...
|
|
76
|
+
]
|
|
77
|
+
"""
|
|
78
|
+
items = self.get_devices_data()
|
|
79
|
+
return [{str(item['id']): item['name']} for item in items]
|