pygpt-net 2.6.66__py3-none-any.whl → 2.7.0__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 (81) hide show
  1. pygpt_net/CHANGELOG.txt +18 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/assistant/assistant.py +13 -8
  4. pygpt_net/controller/assistant/batch.py +29 -15
  5. pygpt_net/controller/assistant/files.py +19 -14
  6. pygpt_net/controller/assistant/store.py +63 -41
  7. pygpt_net/controller/attachment/attachment.py +45 -35
  8. pygpt_net/controller/chat/attachment.py +50 -39
  9. pygpt_net/controller/config/field/dictionary.py +26 -14
  10. pygpt_net/controller/config/field/textarea.py +2 -2
  11. pygpt_net/controller/ctx/common.py +27 -17
  12. pygpt_net/controller/ctx/ctx.py +182 -101
  13. pygpt_net/controller/dialogs/info.py +2 -2
  14. pygpt_net/controller/files/files.py +101 -41
  15. pygpt_net/controller/idx/indexer.py +87 -31
  16. pygpt_net/controller/kernel/kernel.py +13 -2
  17. pygpt_net/controller/media/media.py +29 -1
  18. pygpt_net/controller/mode/mode.py +3 -3
  19. pygpt_net/controller/model/editor.py +141 -21
  20. pygpt_net/controller/model/importer.py +153 -54
  21. pygpt_net/controller/painter/painter.py +2 -2
  22. pygpt_net/controller/presets/experts.py +68 -15
  23. pygpt_net/controller/presets/presets.py +72 -36
  24. pygpt_net/controller/settings/editor.py +25 -1
  25. pygpt_net/controller/settings/profile.py +76 -35
  26. pygpt_net/controller/settings/workdir.py +70 -39
  27. pygpt_net/core/assistants/files.py +20 -18
  28. pygpt_net/core/filesystem/actions.py +111 -10
  29. pygpt_net/core/filesystem/filesystem.py +2 -1
  30. pygpt_net/core/idx/idx.py +12 -11
  31. pygpt_net/core/idx/worker.py +13 -1
  32. pygpt_net/core/models/models.py +4 -4
  33. pygpt_net/core/profile/profile.py +13 -3
  34. pygpt_net/core/types/image.py +10 -1
  35. pygpt_net/core/video/video.py +43 -3
  36. pygpt_net/data/config/config.json +3 -3
  37. pygpt_net/data/config/models.json +25 -14
  38. pygpt_net/data/css/style.dark.css +39 -1
  39. pygpt_net/data/css/style.light.css +39 -1
  40. pygpt_net/data/locale/locale.de.ini +4 -1
  41. pygpt_net/data/locale/locale.en.ini +4 -1
  42. pygpt_net/data/locale/locale.es.ini +4 -1
  43. pygpt_net/data/locale/locale.fr.ini +4 -1
  44. pygpt_net/data/locale/locale.it.ini +4 -1
  45. pygpt_net/data/locale/locale.pl.ini +5 -2
  46. pygpt_net/data/locale/locale.uk.ini +4 -1
  47. pygpt_net/data/locale/locale.zh.ini +4 -1
  48. pygpt_net/item/model.py +1 -1
  49. pygpt_net/provider/api/openai/__init__.py +4 -2
  50. pygpt_net/provider/api/openai/video.py +2 -2
  51. pygpt_net/provider/core/config/patch.py +9 -1
  52. pygpt_net/provider/core/model/patch.py +26 -1
  53. pygpt_net/tools/image_viewer/tool.py +17 -0
  54. pygpt_net/tools/text_editor/tool.py +9 -0
  55. pygpt_net/ui/__init__.py +2 -2
  56. pygpt_net/ui/dialog/models.py +10 -1
  57. pygpt_net/ui/layout/ctx/ctx_list.py +16 -6
  58. pygpt_net/ui/layout/toolbox/video.py +14 -6
  59. pygpt_net/ui/main.py +3 -1
  60. pygpt_net/ui/widget/calendar/select.py +3 -3
  61. pygpt_net/ui/widget/filesystem/explorer.py +1082 -142
  62. pygpt_net/ui/widget/lists/assistant.py +185 -24
  63. pygpt_net/ui/widget/lists/assistant_store.py +245 -42
  64. pygpt_net/ui/widget/lists/attachment.py +230 -47
  65. pygpt_net/ui/widget/lists/attachment_ctx.py +189 -33
  66. pygpt_net/ui/widget/lists/base_list_combo.py +2 -2
  67. pygpt_net/ui/widget/lists/context.py +1253 -70
  68. pygpt_net/ui/widget/lists/experts.py +110 -8
  69. pygpt_net/ui/widget/lists/model_editor.py +217 -14
  70. pygpt_net/ui/widget/lists/model_importer.py +125 -6
  71. pygpt_net/ui/widget/lists/preset.py +460 -71
  72. pygpt_net/ui/widget/lists/profile.py +149 -27
  73. pygpt_net/ui/widget/lists/uploaded.py +230 -38
  74. pygpt_net/ui/widget/option/combo.py +1046 -32
  75. pygpt_net/ui/widget/option/dictionary.py +35 -7
  76. pygpt_net/ui/widget/option/input.py +3 -1
  77. {pygpt_net-2.6.66.dist-info → pygpt_net-2.7.0.dist-info}/METADATA +20 -57
  78. {pygpt_net-2.6.66.dist-info → pygpt_net-2.7.0.dist-info}/RECORD +81 -81
  79. {pygpt_net-2.6.66.dist-info → pygpt_net-2.7.0.dist-info}/LICENSE +0 -0
  80. {pygpt_net-2.6.66.dist-info → pygpt_net-2.7.0.dist-info}/WHEEL +0 -0
  81. {pygpt_net-2.6.66.dist-info → pygpt_net-2.7.0.dist-info}/entry_points.txt +0 -0
@@ -6,13 +6,13 @@
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.09.26 13:00:00 #
9
+ # Updated Date: 2025.12.28 04:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
13
13
  import os
14
14
  from pathlib import Path
15
- from typing import Optional
15
+ from typing import Optional, Union
16
16
  from uuid import uuid4
17
17
 
18
18
  from PySide6.QtCore import QObject, QRunnable, Signal, Slot
@@ -50,9 +50,10 @@ class WorkdirWorker(QRunnable):
50
50
  force: bool = False,
51
51
  current: Optional[str] = None,
52
52
  profile_name: Optional[str] = None,
53
- profile_uuid: Optional[str] = None,
53
+ profile_uuid: Optional[Union[str, list]] = None,
54
54
  profile_new_name: Optional[str] = None,
55
55
  profile_new_path: Optional[str] = None,
56
+ batch: bool = False,
56
57
  ):
57
58
  super().__init__()
58
59
  self.window = window
@@ -65,6 +66,7 @@ class WorkdirWorker(QRunnable):
65
66
  self.profile_new_name = profile_new_name
66
67
  self.profile_new_path = profile_new_path
67
68
  self.signals = WorkerSignals()
69
+ self.batch = batch
68
70
 
69
71
  @Slot()
70
72
  def run(self):
@@ -104,22 +106,34 @@ class WorkdirWorker(QRunnable):
104
106
  profiles = self.window.controller.settings.profile.get_profiles()
105
107
  remove_datadir = True
106
108
  remove_db = True
107
- if uuid in profiles:
108
- profile = profiles[uuid]
109
- name = profile['name']
110
- path = profile['workdir'].replace("%HOME%", str(Path.home()))
111
- # remove profile
112
- if self.window.core.config.profile.remove(uuid):
113
- if not os.path.exists(path) or not os.path.isdir(path):
114
- self.signals.alert.emit(trans("dialog.profile.alert.path.not_exists"))
115
- return
116
- print(f"Clearing workdir: {path}")
117
- self.window.core.filesystem.clear_workdir(
118
- path,
119
- remove_db=remove_db,
120
- remove_datadir=remove_datadir,
121
- )
122
- self.signals.deleted.emit(name)
109
+ uuids = uuid if isinstance(uuid, list) else [uuid]
110
+ for uuid in uuids:
111
+ if uuid in profiles:
112
+ profile = profiles[uuid]
113
+ name = profile['name']
114
+ path = profile['workdir'].replace("%HOME%", str(Path.home()))
115
+ # remove profile
116
+ if self.window.core.config.profile.remove(uuid):
117
+ if not os.path.exists(path) or not os.path.isdir(path):
118
+ if not self.batch:
119
+ self.signals.deleted.emit(name)
120
+ self.signals.alert.emit(trans("dialog.profile.alert.path.not_exists"))
121
+ return
122
+ else:
123
+ print("Directory not exists, ignoring: ", path)
124
+ self.signals.updateGlobalStatus.emit(f"Directory not exists: {path}")
125
+ self.signals.deleted.emit(name)
126
+ return # nothing to delete
127
+ print(f"Clearing workdir: {path}")
128
+ try:
129
+ self.window.core.filesystem.clear_workdir(
130
+ path,
131
+ remove_db=remove_db,
132
+ remove_datadir=remove_datadir,
133
+ )
134
+ except Exception as e:
135
+ print("Error deleting profile files: ", e)
136
+ self.signals.deleted.emit(name)
123
137
 
124
138
  def worker_duplicate(self):
125
139
  """Duplicate profile"""
@@ -168,22 +182,30 @@ class WorkdirWorker(QRunnable):
168
182
  current = self.window.core.config.profile.get_current()
169
183
  remove_datadir = False
170
184
  remove_db = True
171
- if uuid in profiles:
172
- profile = profiles[uuid]
173
- path = profile['workdir'].replace("%HOME%", str(Path.home()))
174
- if not os.path.exists(path) or not os.path.isdir(path):
175
- self.signals.alert.emit(f"Directory not exists: {path}")
176
- return
177
- print("Clearing workdir: ", path)
178
- self.window.core.db.close()
179
- self.window.core.filesystem.clear_workdir(
180
- path,
181
- remove_db=remove_db,
182
- remove_datadir=remove_datadir,
183
- )
184
- if uuid == current:
185
- self.signals.switch.emit(uuid) # switch to profile
186
- self.signals.updateGlobalStatus.emit(f"Profile cleared: {profile['name']}")
185
+ uuids = uuid if isinstance(uuid, list) else [uuid]
186
+ for uuid in uuids:
187
+ if uuid in profiles:
188
+ profile = profiles[uuid]
189
+ path = profile['workdir'].replace("%HOME%", str(Path.home()))
190
+ if not os.path.exists(path) or not os.path.isdir(path):
191
+ if not self.batch:
192
+ self.signals.alert.emit(f"Directory not exists: {path}")
193
+ return
194
+ else:
195
+ print("Directory not exists, ignoring: ", path)
196
+ self.signals.updateGlobalStatus.emit(f"Directory not exists: {path}")
197
+ return
198
+ print("Clearing workdir: ", path)
199
+ if uuid == current:
200
+ self.window.core.db.close()
201
+ self.window.core.filesystem.clear_workdir(
202
+ path,
203
+ remove_db=remove_db,
204
+ remove_datadir=remove_datadir,
205
+ )
206
+ if uuid == current and not self.batch:
207
+ self.signals.switch.emit(uuid) # switch to profile
208
+ self.signals.updateGlobalStatus.emit(f"Profile cleared: {profile['name']}")
187
209
 
188
210
  def worker_migrate(self):
189
211
  """Migrate working directory"""
@@ -471,9 +493,10 @@ class Workdir:
471
493
  force: bool = False,
472
494
  current: str = None,
473
495
  profile_name: Optional[str] = None,
474
- profile_uuid: Optional[str] = None,
496
+ profile_uuid: Optional[Union[str, list]] = None,
475
497
  profile_new_name: Optional[str] = None,
476
498
  profile_new_path: Optional[str] = None,
499
+ batch: bool = False,
477
500
  ):
478
501
  """
479
502
  Run action in thread
@@ -483,9 +506,10 @@ class Workdir:
483
506
  :param force: force action (confirm)
484
507
  :param current: current working directory (for restore action)
485
508
  :param profile_name: profile name (optional, for after update callback)
486
- :param profile_uuid: profile UUID (optional, for delete files action)
509
+ :param profile_uuid: profile UUID (optional, for delete files action) or list of UUIDs
487
510
  :param profile_new_name: new profile name (optional, for duplicate action)
488
511
  :param profile_new_path: new profile path (optional, for duplicate action)
512
+ :param batch: if True, run in batch mode (no UI updates)
489
513
  """
490
514
  worker = WorkdirWorker(
491
515
  window=self.window,
@@ -497,6 +521,7 @@ class Workdir:
497
521
  profile_uuid=profile_uuid,
498
522
  profile_new_name=profile_new_name,
499
523
  profile_new_path=profile_new_path,
524
+ batch=batch,
500
525
  )
501
526
 
502
527
  # connect signals
@@ -569,16 +594,19 @@ class Workdir:
569
594
 
570
595
  def delete_files(
571
596
  self,
572
- profile_uuid: str
597
+ profile_uuid: Union[str, list],
598
+ batch: bool = False
573
599
  ):
574
600
  """
575
601
  Delete files and directories associated with the profile
576
602
 
577
603
  :param profile_uuid: profile UUID
604
+ :param batch: if True, run in batch mode (no UI updates)
578
605
  """
579
606
  self.run_action(
580
607
  action="delete",
581
608
  profile_uuid=profile_uuid,
609
+ batch=batch,
582
610
  )
583
611
 
584
612
  def duplicate(
@@ -603,14 +631,17 @@ class Workdir:
603
631
 
604
632
  def reset(
605
633
  self,
606
- profile_uuid: str
634
+ profile_uuid: Union[str, list],
635
+ batch: bool = False
607
636
  ):
608
637
  """
609
638
  Reset profile
610
639
 
611
640
  :param profile_uuid: profile UUID to reset
641
+ :param batch: if True, run in batch mode (no UI updates)
612
642
  """
613
643
  self.run_action(
614
644
  action="reset",
615
645
  profile_uuid=profile_uuid,
646
+ batch=batch,
616
647
  )
@@ -6,10 +6,10 @@
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.12.27 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from typing import Optional, List, Dict
12
+ from typing import Optional, List, Dict, Union
13
13
 
14
14
  from packaging.version import Version
15
15
 
@@ -231,30 +231,32 @@ class Files:
231
231
  """
232
232
  return self.provider.get_all_by_file_id(file_id)
233
233
 
234
- def delete(self, file: AssistantFileItem) -> bool:
234
+ def delete(self, file: Union[AssistantFileItem, list]) -> bool:
235
235
  """
236
236
  Delete file and remove from vector stores if exists
237
237
 
238
238
  :param file: file item
239
239
  :return: True if file was deleted
240
240
  """
241
- file_id = file.file_id
242
- items = self.get_all_by_file_id(file_id) # get store_ids
243
- for id in items:
244
- store_id = items[id].store_id
245
- if store_id is None or store_id == "":
246
- continue # skip if no store_id
241
+ files = file if isinstance(file, list) else [file]
242
+ for file in files:
243
+ file_id = file.file_id
244
+ items = self.get_all_by_file_id(file_id) # get store_ids
245
+ for id in items:
246
+ store_id = items[id].store_id
247
+ if store_id is None or store_id == "":
248
+ continue # skip if no store_id
249
+ try:
250
+ self.window.core.api.openai.store.delete_store_file(store_id, file_id) # remove from vector store
251
+ except Exception as e:
252
+ self.window.core.debug.log("Failed to delete file from vector store: " + str(e))
253
+ self.provider.delete_by_id(file.record_id) # delete file in DB
247
254
  try:
248
- self.window.core.api.openai.store.delete_store_file(store_id, file_id) # remove from vector store
255
+ self.window.core.api.openai.store.delete_file(file.file_id) # delete file in API
249
256
  except Exception as e:
250
- self.window.core.debug.log("Failed to delete file from vector store: " + str(e))
251
- self.provider.delete_by_id(file.record_id) # delete file in DB
252
- try:
253
- self.window.core.api.openai.store.delete_file(file.file_id) # delete file in API
254
- except Exception as e:
255
- self.window.core.debug.log("Failed to delete remote file: " + str(e))
256
- if file.record_id in self.items:
257
- del self.items[file.record_id]
257
+ self.window.core.debug.log("Failed to delete remote file: " + str(e))
258
+ if file.record_id in self.items:
259
+ del self.items[file.record_id]
258
260
  return True
259
261
 
260
262
  def delete_by_file_id(self, file_id: str) -> bool:
@@ -6,11 +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.24 23:00:00 #
9
+ # Updated Date: 2025.12.27 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
13
- from typing import List
13
+ from typing import List, Union
14
14
 
15
15
  from PySide6.QtGui import QAction, QIcon
16
16
  from PySide6.QtWidgets import QWidget
@@ -27,16 +27,22 @@ class Actions:
27
27
  """
28
28
  self.window = window
29
29
 
30
- def has_preview(self, path: str) -> bool:
30
+ def has_preview(self, path: Union[str, list]) -> bool:
31
31
  """
32
32
  Check if file has preview action
33
33
 
34
- :param path: path to file
34
+ :param path: path to file or list of paths
35
35
  :return: True if file has preview
36
36
  """
37
- if os.path.isdir(path):
37
+ if isinstance(path, list):
38
+ for p in path:
39
+ if not os.path.isdir(p):
40
+ return True # allow preview for any file in the list
38
41
  return False
39
- return True
42
+ else:
43
+ if os.path.isdir(path):
44
+ return False
45
+ return True
40
46
 
41
47
  def has_use(self, path: str) -> bool:
42
48
  """
@@ -48,14 +54,93 @@ class Actions:
48
54
  return (self.window.core.filesystem.types.is_image(path)
49
55
  or self.window.core.filesystem.types.is_video(path))
50
56
 
51
- def get_preview(self, parent: QWidget, path: str) -> List[QAction]:
57
+ def get_preview_batch(self, parent: QWidget, path: list) -> List[QAction]:
58
+ """
59
+ Get preview actions for multiple files
60
+
61
+ :param parent: explorer widget
62
+ :param path: list of paths
63
+ :return: list of context menu actions
64
+ """
65
+ actions = []
66
+ paths_video = []
67
+ paths_image = []
68
+ paths_edit = []
69
+ for p in path:
70
+ if os.path.isdir(p):
71
+ continue
72
+ actions = []
73
+ if (self.window.core.filesystem.types.is_video(p)
74
+ or self.window.core.filesystem.types.is_audio(p)):
75
+ paths_video.append(p)
76
+ elif self.window.core.filesystem.types.is_image(p):
77
+ paths_image.append(p)
78
+ else:
79
+ extra_excluded = ["pdf", "docx", "doc", "xlsx", "xls", "pptx", "ppt"]
80
+ ext = os.path.splitext(p)[1][1:].lower()
81
+ if ext not in self.window.core.filesystem.types.get_excluded_extensions() + extra_excluded:
82
+ paths_edit.append(p)
83
+
84
+ # video/audio - single action for first file
85
+ if paths_video:
86
+ p = paths_video[0] # single action for first video
87
+ action = QAction(
88
+ QIcon(":/icons/video.svg"),
89
+ trans('action.video.play'),
90
+ parent,
91
+ )
92
+ action.triggered.connect(
93
+ lambda: self.window.tools.get("player").play(p),
94
+ )
95
+ actions.append(action)
96
+ action = QAction(
97
+ QIcon(":/icons/hearing.svg"),
98
+ trans('action.video.transcribe'),
99
+ parent,
100
+ )
101
+ action.triggered.connect(
102
+ lambda: self.window.tools.get("transcriber").from_file(p),
103
+ )
104
+ actions.append(action)
105
+
106
+ # image - batch preview
107
+ if paths_image:
108
+ action = QAction(
109
+ QIcon(":/icons/image.svg"),
110
+ trans('action.preview'),
111
+ parent,
112
+ )
113
+ action.triggered.connect(
114
+ lambda: self.window.tools.get("viewer").open_preview_batch(paths_image),
115
+ )
116
+ actions.append(action)
117
+
118
+ # edit - batch edit
119
+ if paths_edit:
120
+ action = QAction(
121
+ QIcon(":/icons/edit.svg"),
122
+ trans('action.edit'),
123
+ parent,
124
+ )
125
+ action.triggered.connect(
126
+ lambda: self.window.tools.get("editor").open_batch(paths_edit),
127
+ )
128
+ actions.append(action)
129
+
130
+ return actions
131
+
132
+
133
+ def get_preview(self, parent: QWidget, path: Union[str, list]) -> List[QAction]:
52
134
  """
53
135
  Get preview actions for context menu
54
136
 
55
137
  :param parent: explorer widget
56
- :param path: path to file
138
+ :param path: path to file or list of paths
57
139
  :return: list of context menu actions
58
140
  """
141
+ if isinstance(path, list):
142
+ return self.get_preview_batch(parent, path)
143
+
59
144
  actions = []
60
145
  if (self.window.core.filesystem.types.is_video(path)
61
146
  or self.window.core.filesystem.types.is_audio(path)):
@@ -102,14 +187,30 @@ class Actions:
102
187
  actions.append(action)
103
188
  return actions
104
189
 
105
- def get_use(self, parent: QWidget, path: str) -> List[QAction]:
190
+ def get_use_batch(self, parent: QWidget, path: list) -> List[QAction]:
191
+ """
192
+ Get use actions for multiple files
193
+
194
+ :param parent: explorer widget
195
+ :param path: list of paths
196
+ :return: list of context menu actions
197
+ """
198
+ actions = []
199
+ for p in path:
200
+ actions.extend(self.get_use(parent, p))
201
+ return actions
202
+
203
+ def get_use(self, parent: QWidget, path: Union[str, list]) -> List[QAction]:
106
204
  """
107
205
  Get use actions for context menu
108
206
 
109
207
  :param parent: explorer widget
110
- :param path: path to file
208
+ :param path: path to file or list of paths
111
209
  :return: list of context menu actions
112
210
  """
211
+ if isinstance(path, list):
212
+ return self.get_use_batch(parent, path)
213
+
113
214
  actions = []
114
215
  if self.window.core.filesystem.types.is_image(path):
115
216
  action = QAction(
@@ -429,7 +429,8 @@ class Filesystem:
429
429
  os.remove(item_path)
430
430
  else:
431
431
  if item not in excluded_dirs:
432
- shutil.rmtree(item_path)
432
+ if os.path.exists(item_path) and os.path.isdir(item_path):
433
+ shutil.rmtree(item_path)
433
434
 
434
435
  return True
435
436
 
pygpt_net/core/idx/idx.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.08.21 07:00:00 #
9
+ # Updated Date: 2025.12.27 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import datetime
@@ -505,16 +505,17 @@ class Idx:
505
505
  self.llm.get_service_context(stream=False) # init environment only (ENV API keys, etc.)
506
506
  store_id = self.get_current_store()
507
507
  if store_id in self.items and idx in self.items[store_id]:
508
- if file in self.items[store_id][idx].items:
509
- # remove from storage
510
- doc_id = self.items[store_id][idx].items[file]["id"]
511
- self.storage.remove_document(
512
- id=idx,
513
- doc_id=doc_id,
514
- )
515
- # remove from index data and db
516
- del self.items[store_id][idx].items[file]
517
- self.files.remove(store_id, idx, doc_id)
508
+ for basename in list(self.items[store_id][idx].items.keys()):
509
+ item = self.items[store_id][idx].items[basename]
510
+ if file == item["path"]:
511
+ doc_id = item ["id"]
512
+ self.storage.remove_document(
513
+ id=idx,
514
+ doc_id=doc_id,
515
+ )
516
+ # remove from index data and db
517
+ del self.items[store_id][idx].items[basename]
518
+ self.files.remove(store_id, idx, doc_id)
518
519
 
519
520
  def load(self):
520
521
  """Load indexes and indexed items"""
@@ -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.08.21 07:00:00 #
9
+ # Updated Date: 2025.12.27 19:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import QObject, Signal, QRunnable, Slot
@@ -29,6 +29,7 @@ class IndexWorker(QRunnable):
29
29
  self.replace = None
30
30
  self.recursive = None
31
31
  self.from_ts = 0
32
+ self.from_ts_batch = {}
32
33
  self.idx = None
33
34
  self.type = None
34
35
  self.silent = False
@@ -70,6 +71,17 @@ class IndexWorker(QRunnable):
70
71
  self.content,
71
72
  self.from_ts,
72
73
  )
74
+ elif self.type == "db_meta_batch":
75
+ result = 0
76
+ errors = []
77
+ for meta_id in self.content:
78
+ r, e = self.window.core.idx.index_db_by_meta_id(
79
+ self.idx,
80
+ meta_id,
81
+ self.from_ts_batch[meta_id],
82
+ )
83
+ result += r
84
+ errors.extend(e)
73
85
  elif self.type == "db_current":
74
86
  result, errors = self.window.core.idx.index_db_from_updated_ts(
75
87
  self.idx,
@@ -6,11 +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.09.17 19:00:00 #
9
+ # Updated Date: 2025.12.26 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
13
- from typing import Optional, List, Dict
13
+ from typing import Optional, List, Dict, Tuple
14
14
 
15
15
  from httpx_socks import SyncProxyTransport
16
16
  from openai import DefaultHttpxClient
@@ -272,7 +272,7 @@ class Models:
272
272
  """
273
273
  return self.multimodal
274
274
 
275
- def create_empty(self, append: bool = True) -> ModelItem:
275
+ def create_empty(self, append: bool = True) -> Tuple[ModelItem, str]:
276
276
  """
277
277
  Create new empty model
278
278
 
@@ -288,7 +288,7 @@ class Models:
288
288
  model.output = ["text"]
289
289
  if append:
290
290
  self.items[id] = model
291
- return model
291
+ return model, id
292
292
 
293
293
  def get_all(self) -> Dict[str, ModelItem]:
294
294
  """
@@ -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: 2024.12.14 22:00:00 #
9
+ # Updated Date: 2025.12.28 04:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
@@ -116,9 +116,19 @@ class Profile:
116
116
  'profiles': self.profiles
117
117
  }
118
118
  json_data = json.dumps(config, indent=4)
119
+ f_lock = os.path.join(self.base_workdir, self.PROFILE_FILE + '.lock')
119
120
  f = os.path.join(self.base_workdir, self.PROFILE_FILE)
120
- with open(f, 'w', encoding='utf-8') as f:
121
- f.write(json_data)
121
+ # check lock first
122
+ if os.path.exists(f_lock):
123
+ print("WARNING: Profile save aborted, lock exists. Please remove lock file:", f_lock)
124
+ return # abort if lock exists
125
+ with open(f_lock, 'w', encoding='utf-8') as file:
126
+ file.write('lock')
127
+ with open(f, 'w', encoding='utf-8') as file:
128
+ file.write(json_data)
129
+ # remove lock
130
+ if os.path.exists(f_lock):
131
+ os.remove(f_lock)
122
132
 
123
133
  def add(
124
134
  self,
@@ -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.12.25 20:00:00 #
9
+ # Updated Date: 2025.12.26 12:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  VIDEO_AVAILABLE_ASPECT_RATIOS = {
@@ -22,6 +22,15 @@ VIDEO_AVAILABLE_ASPECT_RATIOS = {
22
22
  "21:9": "21:9",
23
23
  }
24
24
 
25
+ VIDEO_AVAILABLE_RESOLUTIONS = {
26
+ "480p": "480p",
27
+ "720p": "720p",
28
+ "1080p": "1080p",
29
+ "1440p": "1440p",
30
+ "4K": "4K",
31
+ "8K": "8K",
32
+ }
33
+
25
34
 
26
35
  IMAGE_AVAILABLE_RESOLUTIONS = {
27
36
  "gpt-image": {