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.
Files changed (44) hide show
  1. pygpt_net/CHANGELOG.txt +12 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/assistant/batch.py +14 -4
  4. pygpt_net/controller/assistant/files.py +1 -0
  5. pygpt_net/controller/assistant/store.py +195 -1
  6. pygpt_net/controller/camera/camera.py +1 -1
  7. pygpt_net/controller/chat/attachment.py +2 -0
  8. pygpt_net/controller/chat/common.py +50 -46
  9. pygpt_net/controller/config/placeholder.py +95 -75
  10. pygpt_net/controller/dialogs/confirm.py +3 -1
  11. pygpt_net/controller/media/media.py +11 -3
  12. pygpt_net/controller/painter/common.py +227 -10
  13. pygpt_net/controller/painter/painter.py +4 -12
  14. pygpt_net/core/assistants/files.py +18 -0
  15. pygpt_net/core/camera/camera.py +38 -93
  16. pygpt_net/core/camera/worker.py +430 -0
  17. pygpt_net/core/filesystem/url.py +3 -0
  18. pygpt_net/core/render/web/body.py +65 -9
  19. pygpt_net/core/text/utils.py +3 -0
  20. pygpt_net/data/config/config.json +234 -221
  21. pygpt_net/data/config/models.json +179 -180
  22. pygpt_net/data/config/settings.json +10 -5
  23. pygpt_net/data/locale/locale.de.ini +8 -6
  24. pygpt_net/data/locale/locale.en.ini +9 -5
  25. pygpt_net/data/locale/locale.es.ini +8 -6
  26. pygpt_net/data/locale/locale.fr.ini +8 -6
  27. pygpt_net/data/locale/locale.it.ini +8 -6
  28. pygpt_net/data/locale/locale.pl.ini +8 -6
  29. pygpt_net/data/locale/locale.uk.ini +8 -6
  30. pygpt_net/data/locale/locale.zh.ini +8 -6
  31. pygpt_net/item/assistant.py +13 -1
  32. pygpt_net/provider/api/google/__init__.py +32 -23
  33. pygpt_net/provider/api/openai/store.py +45 -1
  34. pygpt_net/provider/llms/google.py +4 -0
  35. pygpt_net/ui/dialog/assistant_store.py +213 -203
  36. pygpt_net/ui/layout/chat/input.py +3 -3
  37. pygpt_net/ui/widget/draw/painter.py +458 -75
  38. pygpt_net/ui/widget/option/combo.py +5 -1
  39. pygpt_net/ui/widget/textarea/input.py +273 -3
  40. {pygpt_net-2.6.32.dist-info → pygpt_net-2.6.34.dist-info}/METADATA +14 -2
  41. {pygpt_net-2.6.32.dist-info → pygpt_net-2.6.34.dist-info}/RECORD +44 -43
  42. {pygpt_net-2.6.32.dist-info → pygpt_net-2.6.34.dist-info}/LICENSE +0 -0
  43. {pygpt_net-2.6.32.dist-info → pygpt_net-2.6.34.dist-info}/WHEEL +0 -0
  44. {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: 2024.12.14 08:00:00 #
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: int
41
- :param height: int
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 = self.window.ui.nodes['painter.select.canvas.size'].currentData()
77
- if selected:
78
- size = self.convert_to_size(selected)
79
- self.window.ui.nodes['painter.select.canvas.size'].setCurrentText(selected)
80
- self.set_canvas_size(size[0], size[1])
81
- self.window.core.config.set('painter.canvas.size', selected)
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
- self.window.ui.nodes['painter.select.brush.size'].findText(str(size))
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
- #!/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: 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
- self.window.ui.painter.image.load(path)
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
@@ -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.08.11 14:00:00 #
9
+ # Updated Date: 2025.09.02 16:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
13
- import time
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
- class CaptureSignals(QObject):
37
- finished = Signal()
38
- unfinished = Signal()
39
- destroyed = Signal()
40
- started = Signal()
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
- def setup_camera(self):
59
- """Initialize camera"""
43
+ 'id' is the ordinal index used by vision.capture.idx.
44
+ """
60
45
  try:
61
- import cv2
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
- if self.signals is not None:
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
- import cv2
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
- if self.signals is not None:
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
- def cleanup(self):
127
- """Cleanup resources after worker execution."""
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
- sig.deleteLater()
133
- except RuntimeError:
134
- pass
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]