pygpt-net 2.5.89__py3-none-any.whl → 2.5.91__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 (47) hide show
  1. pygpt_net/CHANGELOG.txt +11 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/access/voice.py +2 -3
  4. pygpt_net/controller/audio/audio.py +7 -4
  5. pygpt_net/controller/chat/output.py +2 -2
  6. pygpt_net/controller/ctx/extra.py +2 -7
  7. pygpt_net/controller/kernel/kernel.py +1 -0
  8. pygpt_net/controller/layout/layout.py +10 -4
  9. pygpt_net/controller/ui/tabs.py +53 -3
  10. pygpt_net/controller/ui/ui.py +2 -0
  11. pygpt_net/core/audio/audio.py +66 -1
  12. pygpt_net/core/audio/backend/native.py +92 -30
  13. pygpt_net/core/tabs/tabs.py +61 -1
  14. pygpt_net/data/config/config.json +4 -3
  15. pygpt_net/data/config/models.json +168 -3
  16. pygpt_net/data/config/settings.json +29 -10
  17. pygpt_net/data/css/style.dark.css +3 -0
  18. pygpt_net/data/css/style.light.css +3 -0
  19. pygpt_net/data/css/web-blocks.css +35 -0
  20. pygpt_net/data/css/web-blocks.dark.css +4 -0
  21. pygpt_net/data/css/web-blocks.light.css +5 -0
  22. pygpt_net/data/locale/locale.de.ini +4 -0
  23. pygpt_net/data/locale/locale.en.ini +4 -0
  24. pygpt_net/data/locale/locale.es.ini +4 -0
  25. pygpt_net/data/locale/locale.fr.ini +4 -0
  26. pygpt_net/data/locale/locale.it.ini +4 -0
  27. pygpt_net/data/locale/locale.pl.ini +4 -0
  28. pygpt_net/data/locale/locale.uk.ini +4 -0
  29. pygpt_net/data/locale/locale.zh.ini +4 -0
  30. pygpt_net/plugin/audio_output/plugin.py +16 -1
  31. pygpt_net/provider/audio_output/eleven_labs.py +2 -2
  32. pygpt_net/provider/audio_output/google_tts.py +2 -2
  33. pygpt_net/provider/audio_output/ms_azure_tts.py +2 -2
  34. pygpt_net/provider/audio_output/openai_tts.py +2 -2
  35. pygpt_net/provider/core/config/json_file.py +10 -2
  36. pygpt_net/provider/core/config/patch.py +17 -1
  37. pygpt_net/provider/core/model/patch.py +10 -0
  38. pygpt_net/tools/code_interpreter/tool.py +33 -21
  39. pygpt_net/ui/layout/chat/chat.py +14 -0
  40. pygpt_net/ui/layout/chat/input.py +6 -1
  41. pygpt_net/ui/layout/chat/output.py +6 -6
  42. pygpt_net/ui/widget/element/labels.py +63 -4
  43. {pygpt_net-2.5.89.dist-info → pygpt_net-2.5.91.dist-info}/METADATA +20 -6
  44. {pygpt_net-2.5.89.dist-info → pygpt_net-2.5.91.dist-info}/RECORD +47 -47
  45. {pygpt_net-2.5.89.dist-info → pygpt_net-2.5.91.dist-info}/LICENSE +0 -0
  46. {pygpt_net-2.5.89.dist-info → pygpt_net-2.5.91.dist-info}/WHEEL +0 -0
  47. {pygpt_net-2.5.89.dist-info → pygpt_net-2.5.91.dist-info}/entry_points.txt +0 -0
pygpt_net/CHANGELOG.txt CHANGED
@@ -1,3 +1,14 @@
1
+ 2.5.91 (2025-08-08)
2
+
3
+ - Added GPT-5.
4
+ - Added audio cache - #118.
5
+
6
+ 2.5.90 (2025-08-07)
7
+
8
+ - Fix: Initialize context summary if a conversation starts with a tool call.
9
+ - Fix: Store splitter positions even if the object is deleted from memory.
10
+ - Update: CSS improvements.
11
+
1
12
  2.5.89 (2025-08-07)
2
13
 
3
14
  - Added audio output device selection in Config -> Audio - issue #117
pygpt_net/__init__.py CHANGED
@@ -6,15 +6,15 @@
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.06 00:00:00 #
9
+ # Updated Date: 2025.08.08 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  __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.5.89"
17
- __build__ = "2025-08-07"
16
+ __version__ = "2.5.91"
17
+ __build__ = "2025-08-08"
18
18
  __maintainer__ = "Marcin Szczygliński"
19
19
  __github__ = "https://github.com/szczyglis-dev/py-gpt"
20
20
  __report__ = "https://github.com/szczyglis-dev/py-gpt/issues"
@@ -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.07 03:00:00 #
9
+ # Updated Date: 2025.08.07 22:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Optional, List, Dict, Any
@@ -231,8 +231,7 @@ class Voice(QObject):
231
231
  self.switch_btn_stop()
232
232
 
233
233
  # stop audio output if playing
234
- if self.window.controller.audio.is_playing():
235
- self.window.controller.audio.stop_output()
234
+ self.window.controller.audio.stop_output()
236
235
 
237
236
  # set audio volume bar
238
237
  self.window.core.audio.capture.set_bar(
@@ -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.01.17 13:00:00 #
9
+ # Updated Date: 2025.08.07 22:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -291,7 +291,7 @@ class Audio:
291
291
 
292
292
  if use_cache:
293
293
  lang = self.window.core.config.get("lang")
294
- cache_dir = os.path.join(self.window.core.config.get_user_path(), "cache", "audio", lang)
294
+ cache_dir = os.path.join(self.window.core.audio.get_cache_dir(), lang)
295
295
  if not os.path.exists(cache_dir):
296
296
  os.makedirs(cache_dir, exist_ok=True)
297
297
  cache_file = os.path.join(str(cache_dir), event.name + ".wav")
@@ -318,7 +318,7 @@ class Audio:
318
318
  )
319
319
  return
320
320
 
321
- cache_dir = os.path.join(self.window.core.config.get_user_path(), "cache", "audio")
321
+ cache_dir = self.window.core.audio.get_cache_dir()
322
322
  if os.path.exists(cache_dir):
323
323
  import shutil
324
324
  shutil.rmtree(cache_dir)
@@ -352,7 +352,10 @@ class Audio:
352
352
 
353
353
  :param text: text to play
354
354
  """
355
- self.window.update_status(trans("status.audio.start"))
355
+ if text:
356
+ self.window.update_status(trans("status.audio.start"))
357
+ else:
358
+ self.window.update_status("")
356
359
  QApplication.processEvents() # process events to update UI
357
360
 
358
361
  def on_play(self, event: str):
@@ -226,7 +226,7 @@ class Output:
226
226
  self.window.controller.chat.common.unlock_input() # unlock input
227
227
 
228
228
  # handle ctx name (generate title from summary if not initialized)
229
- if not reply and not internal: # don't call if reply or internal mode
229
+ if not ctx.meta or not ctx.meta.initialized: # don't call if reply or internal mode
230
230
  if self.window.core.config.get('ctx.auto_summary'):
231
231
  self.log("Calling for prepare context name...")
232
232
  self.window.controller.ctx.prepare_name(ctx) # async
@@ -295,7 +295,7 @@ class Output:
295
295
  self.window.dispatch(event) # reload chat window
296
296
 
297
297
  # self.window.core.debug.mem("END") # debug memory usage
298
- gc.collect()
298
+ # gc.collect()
299
299
 
300
300
  def log(self, data: Any):
301
301
  """
@@ -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.07.14 18:00:00 #
9
+ # Updated Date: 2025.08.07 22:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtWidgets import QApplication
@@ -220,12 +220,7 @@ class Extra:
220
220
  self.window.controller.audio.read_text(item.output)
221
221
  self.audio_play_id = item_id
222
222
  elif self.audio_play_id == item_id:
223
- if not self.window.controller.audio.is_playing():
224
- self.window.controller.audio.read_text(item.output)
225
- else:
226
- self.window.controller.audio.stop_output()
227
- self.window.controller.audio.on_stop()
228
- self.audio_play_id = None
223
+ self.window.controller.audio.read_text(item.output)
229
224
 
230
225
  def join_item(
231
226
  self,
@@ -52,6 +52,7 @@ class Kernel(QObject):
52
52
  KernelEvent.APPEND_DATA,
53
53
  KernelEvent.INPUT_USER,
54
54
  KernelEvent.FORCE_CALL,
55
+ KernelEvent.STATUS,
55
56
  ]
56
57
 
57
58
  def init(self):
@@ -112,8 +112,11 @@ class Layout:
112
112
  # do not save main splitter state if notepad was not opened yet
113
113
  if splitter == "calendar" and not self.window.controller.notepad.opened_once:
114
114
  continue
115
- if splitter in self.window.ui.splitters:
116
- data[splitter] = self.window.ui.splitters[splitter].sizes()
115
+ try:
116
+ if splitter in self.window.ui.splitters:
117
+ data[splitter] = self.window.ui.splitters[splitter].sizes()
118
+ except Exception as e:
119
+ pass
117
120
  self.window.core.config.set('layout.splitters', data)
118
121
 
119
122
  def splitters_restore(self):
@@ -151,8 +154,11 @@ class Layout:
151
154
 
152
155
  # notepads
153
156
  for id in self.window.ui.notepad:
154
- scroll_id = "notepad." + str(id)
155
- data[scroll_id] = self.window.ui.notepad[id].textarea.verticalScrollBar().value()
157
+ try:
158
+ scroll_id = "notepad." + str(id)
159
+ data[scroll_id] = self.window.ui.notepad[id].textarea.verticalScrollBar().value()
160
+ except Exception as e:
161
+ pass
156
162
  self.window.core.config.set('layout.scroll', data)
157
163
 
158
164
  def scroll_restore(self):
@@ -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: 2025.07.20 23:00:00 #
9
+ # Updated Date: 2025.08.07 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
- from typing import Any, Optional
12
+ from typing import Any, Optional, Tuple
13
13
 
14
14
  from PySide6.QtCore import QTimer
15
15
 
@@ -178,6 +178,7 @@ class Tabs:
178
178
 
179
179
  prev_tab = self.current
180
180
  prev_column = self.column_idx
181
+
181
182
  self.current = idx
182
183
  self.column_idx = column_idx
183
184
  self.window.controller.ui.mode.update()
@@ -287,6 +288,38 @@ class Tabs:
287
288
  """
288
289
  return self.window.core.tabs.get_min_idx_by_type(type, self.column_idx)
289
290
 
291
+ def get_prev_idx_from(self, idx: int) -> Tuple[int, bool]:
292
+ """
293
+ Get previous tab index from given index
294
+
295
+ :param idx: tab index
296
+ :return: tuple of previous index and boolean indicating if it exists
297
+ """
298
+ return self.window.core.tabs.get_prev_idx_from(idx, self.column_idx)
299
+
300
+ def get_next_idx_from(self, idx: int) -> Tuple[int, bool]:
301
+ """
302
+ Get next tab index from given index
303
+
304
+ :param idx: tab index
305
+ :return: tuple of next index and boolean indicating if it exists
306
+ """
307
+ return self.window.core.tabs.get_next_idx_from(idx, self.column_idx)
308
+
309
+ def get_after_close_idx(self, idx: int) -> int:
310
+ """
311
+ Get tab index after closing the given index
312
+
313
+ :param idx: tab index
314
+ :return: previous tab index if exists, otherwise None
315
+ """
316
+ prev_idx, exists = self.get_prev_idx_from(idx)
317
+ if exists:
318
+ return prev_idx
319
+ next_idx, exists = self.get_next_idx_from(idx)
320
+ if exists:
321
+ return next_idx
322
+
290
323
  def on_column_changed(self):
291
324
  """Column changed event"""
292
325
  if self.locked:
@@ -381,7 +414,21 @@ class Tabs:
381
414
  """
382
415
  if self.locked:
383
416
  return
417
+
418
+ previous_current = self.current
419
+ idx_after = None # <--- next tab index after close to switch to
420
+ if previous_current != idx and self.column_idx == column_idx:
421
+ idx_after = previous_current
422
+ if idx_after > idx:
423
+ idx_after -= 1 # if current is after closed tab, idx will be shifted
424
+
425
+ if idx_after is None:
426
+ idx_after = self.get_after_close_idx(idx) # find next tab index after close
427
+
384
428
  self.window.core.tabs.remove_tab_by_idx(idx, column_idx)
429
+ if idx_after is not None:
430
+ self.switch_tab_by_idx(idx_after, column_idx)
431
+
385
432
  self.on_changed()
386
433
  self.update_current()
387
434
  self.debug()
@@ -639,7 +686,10 @@ class Tabs:
639
686
 
640
687
  :param column_idx: column index
641
688
  """
642
- idx = self.get_current_idx(column_idx)
689
+ # append at the end of column
690
+ idx = self.window.core.tabs.get_max_idx_by_column(column_idx)
691
+ if idx == -1:
692
+ idx = 0
643
693
  self.append(
644
694
  type=Tab.TAB_CHAT,
645
695
  tool_id=None,
@@ -42,6 +42,8 @@ class UI:
42
42
  7: {'label': 'label.color.violet', 'color': QColor(238, 130, 238), 'font': QColor(255, 255, 255)},
43
43
  }
44
44
  self.stop_action = None
45
+ self.splitter_output_size_input = None
46
+ self.splitter_output_size_files = None
45
47
 
46
48
  def setup(self):
47
49
  """Setup UI"""
@@ -6,9 +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.07 03:00:00 #
9
+ # Updated Date: 2025.08.07 22:00:00 #
10
10
  # ================================================== #
11
11
 
12
+ import hashlib
13
+ import os
12
14
  import re
13
15
  from typing import Union, Optional, Tuple, List
14
16
 
@@ -21,6 +23,9 @@ from .whisper import Whisper
21
23
 
22
24
 
23
25
  class Audio:
26
+
27
+ CACHE_FORMAT = "mp3" # default cache format
28
+
24
29
  def __init__(self, window=None):
25
30
  """
26
31
  Audio input/output core
@@ -173,3 +178,63 @@ class Audio:
173
178
  :return: Error
174
179
  """
175
180
  return self.last_error
181
+
182
+ def prepare_cache_path(self, content: str) -> Tuple[Union[str, None], bool]:
183
+ """
184
+ Prepare unique cache file name for given content
185
+
186
+ :param content: text content to generate cache file name
187
+ :return: cache file path or None if content is empty
188
+ """
189
+ exists = False
190
+ if not content:
191
+ return None, exists
192
+ sha1sum = hashlib.sha1(content.encode('utf-8')).hexdigest()
193
+ filename = f"{sha1sum}." + self.CACHE_FORMAT
194
+ tmp_dir = self.get_cache_dir()
195
+ path = os.path.join(tmp_dir, filename)
196
+ if os.path.exists(path):
197
+ exists = True
198
+ return str(path), exists
199
+
200
+ def get_cache_dir(self) -> str:
201
+ """
202
+ Get cache directory for audio files
203
+
204
+ :return: audio cache directory path
205
+ """
206
+ dir = self.window.core.config.get_user_dir("tmp")
207
+ tmp_dir = os.path.join(dir, "audio_cache")
208
+ if not os.path.exists(tmp_dir):
209
+ os.makedirs(tmp_dir, exist_ok=True)
210
+ return tmp_dir
211
+
212
+ def mp3_to_wav(
213
+ self,
214
+ src_file: str,
215
+ dst_file: Optional[str] = None
216
+ ) -> Union[str, None]:
217
+ """
218
+ Convert MP3 file to WAV format
219
+
220
+ :param src_file: Path to the source MP3 file
221
+ :param dst_file: Optional path for the destination WAV file.
222
+ :return: Path to the converted WAV file or None if conversion fails.
223
+ """
224
+ from pydub import AudioSegment
225
+ try:
226
+ mp3_audio = AudioSegment.from_mp3(src_file)
227
+ except Exception as e:
228
+ print(f"Error loading mp3 file: {e}")
229
+ print("Please install ffmpeg to handle mp3 files: https://ffmpeg.org/")
230
+ return
231
+ if dst_file is None:
232
+ dir = os.path.dirname(src_file)
233
+ filename = os.path.splitext(os.path.basename(src_file))[0] + ".wav"
234
+ dst_file = os.path.join(dir, filename)
235
+ try:
236
+ mp3_audio.export(dst_file, format="wav")
237
+ return str(dst_file)
238
+ except Exception as e:
239
+ print(f"Error exporting wav file: {e}")
240
+ return
@@ -6,18 +6,21 @@
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.07 03:00:00 #
9
+ # Updated Date: 2025.08.07 22:00:00 #
10
10
  # ================================================== #
11
11
 
12
+ import os
12
13
  import time
13
14
  from typing import List, Tuple
14
15
 
15
16
  from PySide6.QtCore import QTimer, QObject
17
+ from pydub import AudioSegment
16
18
 
17
19
 
18
20
  class NativeBackend(QObject):
19
21
 
20
22
  MIN_FRAMES = 25 # minimum frames to start transcription
23
+ AUTO_CONVERT_TO_WAV = False # automatically convert to WAV format
21
24
 
22
25
  def __init__(self, window=None):
23
26
  """
@@ -42,7 +45,10 @@ class NativeBackend(QObject):
42
45
  self.stop_callback = None
43
46
  self.player = None
44
47
  self.playback_timer = None
48
+ self.volume_timer = None
45
49
  self.audio_output = None
50
+ self.envelope = []
51
+ self.chunk_ms = 10
46
52
 
47
53
  # Get configuration values (use defaults if unavailable)
48
54
  if self.window is not None and hasattr(self.window, "core"):
@@ -457,8 +463,6 @@ class NativeBackend(QObject):
457
463
  :param signals: Signals to emit on playback
458
464
  :return: True if started
459
465
  """
460
- import os
461
- from pydub import AudioSegment
462
466
  from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput, QMediaDevices
463
467
  from PySide6.QtCore import QUrl, QTimer
464
468
  if signals is not None:
@@ -476,42 +480,57 @@ class NativeBackend(QObject):
476
480
  selected_device = devices[num_device] if num_device < len(devices) else devices[0]
477
481
  self.audio_output.setDevice(selected_device)
478
482
 
479
- self.player = QMediaPlayer()
480
- self.player.setAudioOutput(self.audio_output)
481
-
482
- if audio_file.lower().endswith('.mp3'):
483
- try:
484
- mp3_audio = AudioSegment.from_mp3(audio_file)
485
- except Exception as e:
486
- print(f"Error loading mp3 file: {e}")
487
- return
488
-
489
- base_dir = self.window.core.config.get_user_path()
490
- base_name = os.path.splitext(os.path.basename(audio_file))[0]
491
- wav_file = os.path.join(base_dir, "_" + base_name + ".wav")
492
- try:
493
- mp3_audio.export(wav_file, format="wav")
494
- except Exception as e:
495
- print(f"Error exporting wav file: {e}")
496
- return
497
-
498
- self.player.setSource(QUrl.fromLocalFile(wav_file))
499
- else:
500
- self.player.setSource(QUrl.fromLocalFile(audio_file))
501
-
502
- self.player.play()
503
- self.playback_timer = QTimer()
504
- self.playback_timer.setInterval(100)
483
+ if self.AUTO_CONVERT_TO_WAV:
484
+ if audio_file.lower().endswith('.mp3'):
485
+ tmp_dir = self.window.core.audio.get_cache_dir()
486
+ base_name = os.path.splitext(os.path.basename(audio_file))[0]
487
+ dst_file = os.path.join(tmp_dir, "_" + base_name + ".wav")
488
+ wav_file = self.window.core.audio.mp3_to_wav(audio_file, dst_file)
489
+ if wav_file:
490
+ audio_file = wav_file
505
491
 
506
492
  def check_stop():
507
493
  if stopped():
508
494
  self.player.stop()
509
- self.playback_timer.stop()
495
+ self.stop_timers()
496
+ signals.volume_changed.emit(0)
497
+ else:
498
+ if self.player:
499
+ if self.player.playbackState() == QMediaPlayer.StoppedState:
500
+ self.player.stop()
501
+ self.stop_timers()
502
+ signals.volume_changed.emit(0)
510
503
 
504
+ self.envelope = self.calculate_envelope(audio_file, self.chunk_ms)
505
+ self.player = QMediaPlayer()
506
+ self.player.setAudioOutput(self.audio_output)
507
+ self.player.setSource(QUrl.fromLocalFile(audio_file))
508
+ self.player.play()
509
+
510
+ self.playback_timer = QTimer()
511
+ self.playback_timer.setInterval(100)
511
512
  self.playback_timer.timeout.connect(check_stop)
513
+ self.volume_timer = QTimer(self)
514
+ self.volume_timer.setInterval(10) # every 100 ms
515
+ self.volume_timer.timeout.connect(
516
+ lambda: self.update_volume(signals)
517
+ )
518
+
512
519
  self.playback_timer.start()
520
+ self.volume_timer.start()
513
521
  signals.volume_changed.emit(0)
514
522
 
523
+ def stop_timers(self):
524
+ """
525
+ Stop playback timers.
526
+ """
527
+ if self.playback_timer is not None:
528
+ self.playback_timer.stop()
529
+ self.playback_timer = None
530
+ if self.volume_timer is not None:
531
+ self.volume_timer.stop()
532
+ self.volume_timer = None
533
+
515
534
  def play(
516
535
  self,
517
536
  audio_file: str,
@@ -543,6 +562,49 @@ class NativeBackend(QObject):
543
562
  self.playback_timer.stop()
544
563
  return False
545
564
 
565
+ def calculate_envelope(
566
+ self,
567
+ audio_file: str,
568
+ chunk_ms: int = 100
569
+ ) -> list:
570
+ """
571
+ Calculate the volume envelope of an audio file.
572
+
573
+ :param audio_file: Path to the audio file
574
+ :param chunk_ms: Size of each chunk in milliseconds
575
+ """
576
+ import numpy as np
577
+ audio = AudioSegment.from_file(audio_file)
578
+ max_amplitude = 32767
579
+ envelope = []
580
+
581
+ for ms in range(0, len(audio), chunk_ms):
582
+ chunk = audio[ms:ms + chunk_ms]
583
+ rms = chunk.rms
584
+ if rms > 0:
585
+ db = 20 * np.log10(rms / max_amplitude)
586
+ else:
587
+ db = -60
588
+ db = max(-60, min(0, db))
589
+ volume = ((db + 60) / 60) * 100
590
+ envelope.append(volume)
591
+
592
+ return envelope
593
+
594
+ def update_volume(self, signals=None):
595
+ """
596
+ Update the volume based on the current position in the audio file.
597
+
598
+ :param signals: Signals object to emit volume changed event.
599
+ """
600
+ pos = self.player.position()
601
+ index = int(pos / self.chunk_ms)
602
+ if index < len(self.envelope):
603
+ volume = self.envelope[index]
604
+ else:
605
+ volume = 0
606
+ signals.volume_changed.emit(volume)
607
+
546
608
  def get_input_devices(self) -> List[Tuple[int, str]]:
547
609
  """
548
610
  Get input devices
@@ -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.05 21:00:00 #
9
+ # Updated Date: 2025.08.07 18:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import uuid
@@ -447,6 +447,66 @@ class Tabs:
447
447
  column_idx = tab.column_idx
448
448
  return min, column_idx, exists
449
449
 
450
+ def get_prev_idx_from(self, idx: int, column_idx: int = 0) -> Tuple[int, bool]:
451
+ """
452
+ Get previous index from the specified index
453
+
454
+ :param idx: Tab index
455
+ :param column_idx: Column index
456
+ :return: Previous index and exists flag
457
+ """
458
+ tabs = self.window.ui.layout.get_tabs_by_idx(column_idx)
459
+ if idx < 1:
460
+ return -1, False
461
+ prev_idx = idx - 1
462
+ if prev_idx < 0 or prev_idx >= tabs.count():
463
+ return -1, False
464
+ tab = self.get_tab_by_index(prev_idx, column_idx)
465
+ if tab is None:
466
+ return -1, False
467
+ if tab.column_idx != column_idx:
468
+ return -1, False
469
+ return prev_idx, True
470
+
471
+ def get_next_idx_from(self, idx: int, column_idx: int = 0) -> Tuple[int, bool]:
472
+ """
473
+ Get next index from the specified index
474
+
475
+ :param idx: Tab index
476
+ :param column_idx: Column index
477
+ :return: Next index and exists flag
478
+ """
479
+ tabs = self.window.ui.layout.get_tabs_by_idx(column_idx)
480
+ if idx < 0 or idx >= tabs.count():
481
+ return -1, False
482
+ next_idx = idx + 1
483
+ if next_idx < 0 or next_idx >= tabs.count():
484
+ return -1, False
485
+ tab = self.get_tab_by_index(next_idx, column_idx)
486
+ if tab is None:
487
+ return -1, False
488
+ if tab.column_idx != column_idx:
489
+ return -1, False
490
+ return next_idx, True
491
+
492
+ def get_max_idx_by_column(self, column_idx: int = 0) -> int:
493
+ """
494
+ Get max index in column
495
+
496
+ :param column_idx: Column index
497
+ :return: Max index
498
+ """
499
+ tabs = self.window.ui.layout.get_tabs_by_idx(column_idx)
500
+ if tabs.count() == 0:
501
+ return -1
502
+ max_idx = -1
503
+ for idx in range(tabs.count()):
504
+ tab = self.get_tab_by_index(idx, column_idx)
505
+ if tab is not None and tab.column_idx == column_idx:
506
+ if tab.idx > max_idx:
507
+ max_idx = tab.idx
508
+ return max_idx
509
+
450
510
  def update(self):
451
511
  """Update tabs data (pids) from UI (all columns)"""
452
512
  for n in range(0, self.NUM_COLS):
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "__meta__": {
3
- "version": "2.5.89",
4
- "app.version": "2.5.89",
5
- "updated_at": "2025-08-07T00:00:00"
3
+ "version": "2.5.91",
4
+ "app.version": "2.5.91",
5
+ "updated_at": "2025-08-08T00:00:00"
6
6
  },
7
7
  "access.audio.event.speech": false,
8
8
  "access.audio.event.speech.disabled": [],
@@ -94,6 +94,7 @@
94
94
  "attachments_auto_index": true,
95
95
  "attachments_send_clear": true,
96
96
  "attachments_capture_clear": true,
97
+ "audio.cache.enabled": true,
97
98
  "audio.input.backend": "native",
98
99
  "audio.input.channels": 1,
99
100
  "audio.input.continuous": false,