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.
- pygpt_net/CHANGELOG.txt +11 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/controller/access/voice.py +2 -3
- pygpt_net/controller/audio/audio.py +7 -4
- pygpt_net/controller/chat/output.py +2 -2
- pygpt_net/controller/ctx/extra.py +2 -7
- pygpt_net/controller/kernel/kernel.py +1 -0
- pygpt_net/controller/layout/layout.py +10 -4
- pygpt_net/controller/ui/tabs.py +53 -3
- pygpt_net/controller/ui/ui.py +2 -0
- pygpt_net/core/audio/audio.py +66 -1
- pygpt_net/core/audio/backend/native.py +92 -30
- pygpt_net/core/tabs/tabs.py +61 -1
- pygpt_net/data/config/config.json +4 -3
- pygpt_net/data/config/models.json +168 -3
- pygpt_net/data/config/settings.json +29 -10
- pygpt_net/data/css/style.dark.css +3 -0
- pygpt_net/data/css/style.light.css +3 -0
- pygpt_net/data/css/web-blocks.css +35 -0
- pygpt_net/data/css/web-blocks.dark.css +4 -0
- pygpt_net/data/css/web-blocks.light.css +5 -0
- pygpt_net/data/locale/locale.de.ini +4 -0
- pygpt_net/data/locale/locale.en.ini +4 -0
- pygpt_net/data/locale/locale.es.ini +4 -0
- pygpt_net/data/locale/locale.fr.ini +4 -0
- pygpt_net/data/locale/locale.it.ini +4 -0
- pygpt_net/data/locale/locale.pl.ini +4 -0
- pygpt_net/data/locale/locale.uk.ini +4 -0
- pygpt_net/data/locale/locale.zh.ini +4 -0
- pygpt_net/plugin/audio_output/plugin.py +16 -1
- pygpt_net/provider/audio_output/eleven_labs.py +2 -2
- pygpt_net/provider/audio_output/google_tts.py +2 -2
- pygpt_net/provider/audio_output/ms_azure_tts.py +2 -2
- pygpt_net/provider/audio_output/openai_tts.py +2 -2
- pygpt_net/provider/core/config/json_file.py +10 -2
- pygpt_net/provider/core/config/patch.py +17 -1
- pygpt_net/provider/core/model/patch.py +10 -0
- pygpt_net/tools/code_interpreter/tool.py +33 -21
- pygpt_net/ui/layout/chat/chat.py +14 -0
- pygpt_net/ui/layout/chat/input.py +6 -1
- pygpt_net/ui/layout/chat/output.py +6 -6
- pygpt_net/ui/widget/element/labels.py +63 -4
- {pygpt_net-2.5.89.dist-info → pygpt_net-2.5.91.dist-info}/METADATA +20 -6
- {pygpt_net-2.5.89.dist-info → pygpt_net-2.5.91.dist-info}/RECORD +47 -47
- {pygpt_net-2.5.89.dist-info → pygpt_net-2.5.91.dist-info}/LICENSE +0 -0
- {pygpt_net-2.5.89.dist-info → pygpt_net-2.5.91.dist-info}/WHEEL +0 -0
- {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.
|
|
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.
|
|
17
|
-
__build__ = "2025-08-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
@@ -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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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):
|
pygpt_net/controller/ui/tabs.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,
|
pygpt_net/controller/ui/ui.py
CHANGED
|
@@ -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"""
|
pygpt_net/core/audio/audio.py
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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.
|
|
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
|
pygpt_net/core/tabs/tabs.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.
|
|
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.
|
|
4
|
-
"app.version": "2.5.
|
|
5
|
-
"updated_at": "2025-08-
|
|
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,
|