pygpt-net 2.6.26__py3-none-any.whl → 2.6.28__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 +10 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +5 -1
- pygpt_net/controller/access/voice.py +3 -5
- pygpt_net/controller/audio/audio.py +9 -6
- pygpt_net/controller/audio/ui.py +263 -0
- pygpt_net/controller/chat/common.py +17 -1
- pygpt_net/controller/kernel/kernel.py +2 -0
- pygpt_net/controller/notepad/notepad.py +10 -1
- pygpt_net/controller/theme/markdown.py +2 -0
- pygpt_net/controller/theme/theme.py +4 -1
- pygpt_net/controller/ui/tabs.py +5 -0
- pygpt_net/core/audio/backend/native.py +114 -82
- pygpt_net/core/audio/backend/pyaudio.py +16 -19
- pygpt_net/core/audio/backend/pygame.py +12 -15
- pygpt_net/core/audio/capture.py +10 -9
- pygpt_net/core/audio/context.py +3 -6
- pygpt_net/core/command/command.py +2 -0
- pygpt_net/core/render/web/helpers.py +13 -3
- pygpt_net/core/render/web/renderer.py +3 -3
- pygpt_net/data/config/config.json +7 -5
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +24 -10
- pygpt_net/data/css/web-blocks.darkest.css +91 -0
- pygpt_net/data/css/web-chatgpt.css +7 -5
- pygpt_net/data/css/web-chatgpt.dark.css +5 -2
- pygpt_net/data/css/web-chatgpt.darkest.css +91 -0
- pygpt_net/data/css/web-chatgpt.light.css +8 -2
- pygpt_net/data/css/web-chatgpt_wide.css +7 -4
- pygpt_net/data/css/web-chatgpt_wide.dark.css +5 -2
- pygpt_net/data/css/web-chatgpt_wide.darkest.css +91 -0
- pygpt_net/data/css/web-chatgpt_wide.light.css +9 -6
- pygpt_net/data/locale/locale.de.ini +2 -0
- pygpt_net/data/locale/locale.en.ini +2 -0
- pygpt_net/data/locale/locale.es.ini +2 -0
- pygpt_net/data/locale/locale.fr.ini +2 -0
- pygpt_net/data/locale/locale.it.ini +2 -0
- pygpt_net/data/locale/locale.pl.ini +3 -1
- pygpt_net/data/locale/locale.uk.ini +2 -0
- pygpt_net/data/locale/locale.zh.ini +2 -0
- pygpt_net/data/themes/dark_darkest.css +31 -0
- pygpt_net/data/themes/dark_darkest.xml +10 -0
- pygpt_net/plugin/audio_input/simple.py +5 -10
- pygpt_net/plugin/audio_output/plugin.py +4 -17
- pygpt_net/plugin/tuya/__init__.py +12 -0
- pygpt_net/plugin/tuya/config.py +256 -0
- pygpt_net/plugin/tuya/plugin.py +117 -0
- pygpt_net/plugin/tuya/worker.py +588 -0
- pygpt_net/plugin/wikipedia/__init__.py +12 -0
- pygpt_net/plugin/wikipedia/config.py +228 -0
- pygpt_net/plugin/wikipedia/plugin.py +114 -0
- pygpt_net/plugin/wikipedia/worker.py +430 -0
- pygpt_net/provider/core/config/patch.py +11 -0
- pygpt_net/ui/layout/chat/input.py +5 -2
- pygpt_net/ui/main.py +1 -2
- pygpt_net/ui/widget/audio/bar.py +5 -1
- pygpt_net/ui/widget/tabs/output.py +2 -0
- pygpt_net/ui/widget/textarea/input.py +483 -55
- {pygpt_net-2.6.26.dist-info → pygpt_net-2.6.28.dist-info}/METADATA +78 -35
- {pygpt_net-2.6.26.dist-info → pygpt_net-2.6.28.dist-info}/RECORD +63 -49
- {pygpt_net-2.6.26.dist-info → pygpt_net-2.6.28.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.26.dist-info → pygpt_net-2.6.28.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.26.dist-info → pygpt_net-2.6.28.dist-info}/entry_points.txt +0 -0
|
@@ -6,14 +6,19 @@
|
|
|
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.27 07:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
+
from typing import List, Tuple
|
|
13
|
+
|
|
14
|
+
from bs4 import UnicodeDammit
|
|
12
15
|
import os
|
|
13
16
|
import time
|
|
14
|
-
|
|
17
|
+
import numpy as np
|
|
18
|
+
import wave
|
|
15
19
|
|
|
16
|
-
from PySide6.
|
|
20
|
+
from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput, QMediaDevices, QAudioFormat, QAudioSource
|
|
21
|
+
from PySide6.QtCore import QTimer, QObject, QUrl
|
|
17
22
|
from pydub import AudioSegment
|
|
18
23
|
|
|
19
24
|
|
|
@@ -40,7 +45,6 @@ class NativeBackend(QObject):
|
|
|
40
45
|
self.start_time = 0
|
|
41
46
|
self.devices = []
|
|
42
47
|
self.selected_device = None
|
|
43
|
-
self.bar = None
|
|
44
48
|
self.loop = False
|
|
45
49
|
self.stop_callback = None
|
|
46
50
|
self.player = None
|
|
@@ -49,14 +53,24 @@ class NativeBackend(QObject):
|
|
|
49
53
|
self.audio_output = None
|
|
50
54
|
self.envelope = []
|
|
51
55
|
self.chunk_ms = 10
|
|
56
|
+
self.mode = "input" # input|control
|
|
52
57
|
|
|
53
58
|
# Get configuration values (use defaults if unavailable)
|
|
54
59
|
if self.window is not None and hasattr(self.window, "core"):
|
|
55
60
|
self.channels = int(self.window.core.config.get('audio.input.channels', 1))
|
|
56
61
|
self.rate = int(self.window.core.config.get('audio.input.rate', 44100))
|
|
62
|
+
self.latency_ms = int(self.window.core.config.get('audio.input.latency_ms', 30))
|
|
57
63
|
else:
|
|
58
64
|
self.channels = 1
|
|
59
65
|
self.rate = 44100
|
|
66
|
+
self.latency_ms = 30
|
|
67
|
+
|
|
68
|
+
# internal recording state flag to suppress volume updates after stop
|
|
69
|
+
# This prevents late/queued readyRead events from updating the UI when recording is no longer active.
|
|
70
|
+
self._is_recording = False
|
|
71
|
+
|
|
72
|
+
self._dtype = None
|
|
73
|
+
self._norm = None
|
|
60
74
|
|
|
61
75
|
def init(self):
|
|
62
76
|
"""
|
|
@@ -66,6 +80,14 @@ class NativeBackend(QObject):
|
|
|
66
80
|
self.check_audio_devices()
|
|
67
81
|
self.initialized = True
|
|
68
82
|
|
|
83
|
+
def set_mode(self, mode: str):
|
|
84
|
+
"""
|
|
85
|
+
Set input mode (input|control)
|
|
86
|
+
|
|
87
|
+
:param mode: mode name
|
|
88
|
+
"""
|
|
89
|
+
self.mode = mode
|
|
90
|
+
|
|
69
91
|
def set_repeat_callback(self, callback):
|
|
70
92
|
"""
|
|
71
93
|
Set callback to be called on loop recording
|
|
@@ -93,14 +115,6 @@ class NativeBackend(QObject):
|
|
|
93
115
|
"""
|
|
94
116
|
self.path = path
|
|
95
117
|
|
|
96
|
-
def set_bar(self, bar):
|
|
97
|
-
"""
|
|
98
|
-
Set audio level bar
|
|
99
|
-
|
|
100
|
-
:param bar: audio level bar
|
|
101
|
-
"""
|
|
102
|
-
self.bar = bar
|
|
103
|
-
|
|
104
118
|
def start(self):
|
|
105
119
|
"""
|
|
106
120
|
Start audio input recording
|
|
@@ -108,6 +122,7 @@ class NativeBackend(QObject):
|
|
|
108
122
|
:return: True if started
|
|
109
123
|
"""
|
|
110
124
|
self.init()
|
|
125
|
+
|
|
111
126
|
# Clear previous frames
|
|
112
127
|
self.frames = []
|
|
113
128
|
|
|
@@ -127,6 +142,11 @@ class NativeBackend(QObject):
|
|
|
127
142
|
# Set up audio input and start recording
|
|
128
143
|
self.setup_audio_input()
|
|
129
144
|
self.start_time = time.time()
|
|
145
|
+
|
|
146
|
+
# mark recording as active only after setup succeeded
|
|
147
|
+
# This ensures process_audio_input() will start updating the UI.
|
|
148
|
+
if self.audio_source is not None and self.audio_io_device is not None:
|
|
149
|
+
self._is_recording = True
|
|
130
150
|
return True
|
|
131
151
|
|
|
132
152
|
def stop(self) -> bool:
|
|
@@ -136,9 +156,20 @@ class NativeBackend(QObject):
|
|
|
136
156
|
:return: True if stopped
|
|
137
157
|
"""
|
|
138
158
|
result = False
|
|
159
|
+
|
|
160
|
+
# immediately mark that we are no longer recording
|
|
161
|
+
# This blocks any late UI updates coming from queued signals.
|
|
162
|
+
self._is_recording = False
|
|
163
|
+
|
|
139
164
|
if self.audio_source is not None:
|
|
140
165
|
# Disconnect the readyRead signal
|
|
141
|
-
|
|
166
|
+
try:
|
|
167
|
+
if self.audio_io_device is not None:
|
|
168
|
+
self.audio_io_device.readyRead.disconnect(self.process_audio_input)
|
|
169
|
+
except (TypeError, RuntimeError):
|
|
170
|
+
# ignore if already disconnected or device gone ---
|
|
171
|
+
pass
|
|
172
|
+
|
|
142
173
|
self.audio_source.stop()
|
|
143
174
|
self.audio_source = None
|
|
144
175
|
self.audio_io_device = None
|
|
@@ -149,6 +180,10 @@ class NativeBackend(QObject):
|
|
|
149
180
|
result = True
|
|
150
181
|
else:
|
|
151
182
|
print("No audio data recorded")
|
|
183
|
+
|
|
184
|
+
# reset input volume on stop to visually indicate end of recording ---
|
|
185
|
+
self.reset_audio_level()
|
|
186
|
+
|
|
152
187
|
return result
|
|
153
188
|
|
|
154
189
|
def has_source(self) -> bool:
|
|
@@ -187,8 +222,7 @@ class NativeBackend(QObject):
|
|
|
187
222
|
|
|
188
223
|
def reset_audio_level(self):
|
|
189
224
|
"""Reset the audio level bar"""
|
|
190
|
-
|
|
191
|
-
self.bar.setLevel(0)
|
|
225
|
+
self.window.controller.audio.ui.on_input_volume_change(0, self.mode)
|
|
192
226
|
|
|
193
227
|
def check_audio_input(self) -> bool:
|
|
194
228
|
"""
|
|
@@ -197,24 +231,27 @@ class NativeBackend(QObject):
|
|
|
197
231
|
:return: True if working
|
|
198
232
|
"""
|
|
199
233
|
self.init()
|
|
200
|
-
from PySide6.QtMultimedia import QMediaDevices, QAudioFormat, QAudioSource
|
|
201
|
-
|
|
202
234
|
devices = QMediaDevices.audioInputs()
|
|
203
235
|
if not devices:
|
|
204
236
|
print("No audio input devices found.")
|
|
205
237
|
return False
|
|
206
238
|
|
|
207
239
|
device = self.selected_device if self.selected_device else devices[0]
|
|
208
|
-
|
|
209
|
-
audio_format.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
audio_format =
|
|
240
|
+
|
|
241
|
+
audio_format = device.preferredFormat()
|
|
242
|
+
desired = QAudioFormat()
|
|
243
|
+
desired.setSampleRate(self.rate)
|
|
244
|
+
desired.setChannelCount(self.channels)
|
|
245
|
+
desired.setSampleFormat(QAudioFormat.SampleFormat.Int16)
|
|
246
|
+
if device.isFormatSupported(desired):
|
|
247
|
+
audio_format = desired
|
|
248
|
+
|
|
216
249
|
try:
|
|
217
250
|
audio_source = QAudioSource(device, audio_format)
|
|
251
|
+
bs = int(audio_format.sampleRate() * audio_format.channelCount() * audio_format.bytesPerSample() * (float(self.latency_ms) / 1000.0))
|
|
252
|
+
if bs < 4096:
|
|
253
|
+
bs = 4096
|
|
254
|
+
audio_source.setBufferSize(bs)
|
|
218
255
|
io_device = audio_source.start()
|
|
219
256
|
if io_device is None:
|
|
220
257
|
print("Unable to access audio input device")
|
|
@@ -227,7 +264,6 @@ class NativeBackend(QObject):
|
|
|
227
264
|
|
|
228
265
|
def check_audio_devices(self):
|
|
229
266
|
"""Check audio input devices"""
|
|
230
|
-
from PySide6.QtMultimedia import QMediaDevices
|
|
231
267
|
self.devices = QMediaDevices.audioInputs()
|
|
232
268
|
if not self.devices:
|
|
233
269
|
# no devices found
|
|
@@ -274,39 +310,30 @@ class NativeBackend(QObject):
|
|
|
274
310
|
print("No audio input device selected")
|
|
275
311
|
return
|
|
276
312
|
|
|
277
|
-
from PySide6.QtMultimedia import QAudioFormat, QAudioSource
|
|
278
|
-
|
|
279
|
-
# Define audio format
|
|
280
|
-
audio_format = QAudioFormat()
|
|
281
|
-
audio_format.setSampleRate(int(self.window.core.config.get('audio.input.rate', 44100)))
|
|
282
|
-
audio_format.setChannelCount(int(self.window.core.config.get('audio.input.channels', 1)))
|
|
283
|
-
audio_format.setSampleFormat(QAudioFormat.SampleFormat.Int16)
|
|
284
|
-
|
|
285
|
-
# Select default audio input device
|
|
286
313
|
audio_input_device = self.selected_device
|
|
287
314
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if audio_format.bytesPerSample() > 2:
|
|
297
|
-
audio_format.setSampleFormat(QAudioFormat.SampleFormat.Int16)
|
|
298
|
-
|
|
299
|
-
# Store the actual format being used
|
|
315
|
+
audio_format = audio_input_device.preferredFormat()
|
|
316
|
+
desired = QAudioFormat()
|
|
317
|
+
desired.setSampleRate(int(self.window.core.config.get('audio.input.rate', 44100)))
|
|
318
|
+
desired.setChannelCount(int(self.window.core.config.get('audio.input.channels', 1)))
|
|
319
|
+
desired.setSampleFormat(QAudioFormat.SampleFormat.Int16)
|
|
320
|
+
if audio_input_device.isFormatSupported(desired):
|
|
321
|
+
audio_format = desired
|
|
322
|
+
|
|
300
323
|
self.actual_audio_format = audio_format
|
|
324
|
+
self._dtype = self.get_dtype_from_sample_format(self.actual_audio_format.sampleFormat())
|
|
325
|
+
self._norm = self.get_normalization_factor(self.actual_audio_format.sampleFormat())
|
|
301
326
|
|
|
302
|
-
# Create QAudioSource object with the device and format
|
|
303
327
|
try:
|
|
304
328
|
self.audio_source = QAudioSource(audio_input_device, audio_format)
|
|
329
|
+
bs = int(audio_format.sampleRate() * audio_format.channelCount() * audio_format.bytesPerSample() * (float(self.latency_ms) / 1000.0))
|
|
330
|
+
if bs < 4096:
|
|
331
|
+
bs = 4096
|
|
332
|
+
self.audio_source.setBufferSize(bs)
|
|
305
333
|
except Exception as e:
|
|
306
334
|
self.disconnected = True
|
|
307
335
|
print(f"Failed to create audio source: {e}")
|
|
308
336
|
|
|
309
|
-
# Start audio input and obtain the QIODevice
|
|
310
337
|
try:
|
|
311
338
|
self.audio_io_device = self.audio_source.start()
|
|
312
339
|
if self.audio_io_device is None:
|
|
@@ -318,13 +345,13 @@ class NativeBackend(QObject):
|
|
|
318
345
|
self.audio_io_device = None
|
|
319
346
|
return
|
|
320
347
|
|
|
321
|
-
# Connect the readyRead signal to process incoming data
|
|
322
348
|
self.audio_io_device.readyRead.connect(self.process_audio_input)
|
|
323
349
|
|
|
324
350
|
def process_audio_input(self):
|
|
325
351
|
"""Process incoming audio data"""
|
|
326
|
-
|
|
327
|
-
|
|
352
|
+
# guard against late calls after stop or missing device ---
|
|
353
|
+
if not self._is_recording or self.audio_io_device is None:
|
|
354
|
+
return
|
|
328
355
|
|
|
329
356
|
# add seconds to stop timer
|
|
330
357
|
data = self.audio_io_device.readAll()
|
|
@@ -339,8 +366,8 @@ class NativeBackend(QObject):
|
|
|
339
366
|
|
|
340
367
|
# Determine the correct dtype and normalization factor
|
|
341
368
|
sample_format = self.actual_audio_format.sampleFormat()
|
|
342
|
-
dtype = self.get_dtype_from_sample_format(sample_format)
|
|
343
|
-
normalization_factor = self.get_normalization_factor(sample_format)
|
|
369
|
+
dtype = self._dtype if self._dtype is not None else self.get_dtype_from_sample_format(sample_format)
|
|
370
|
+
normalization_factor = self._norm if self._norm is not None else self.get_normalization_factor(sample_format)
|
|
344
371
|
|
|
345
372
|
# Convert bytes to NumPy array of the appropriate type
|
|
346
373
|
samples = np.frombuffer(data_bytes, dtype=dtype)
|
|
@@ -383,8 +410,10 @@ class NativeBackend(QObject):
|
|
|
383
410
|
|
|
384
411
|
:param level: audio level
|
|
385
412
|
"""
|
|
386
|
-
if
|
|
387
|
-
|
|
413
|
+
# do not update UI if recording already stopped
|
|
414
|
+
if not self._is_recording:
|
|
415
|
+
return
|
|
416
|
+
self.window.controller.audio.ui.on_input_volume_change(int(level), self.mode)
|
|
388
417
|
|
|
389
418
|
def save_audio_file(self, filename: str):
|
|
390
419
|
"""
|
|
@@ -392,20 +421,40 @@ class NativeBackend(QObject):
|
|
|
392
421
|
|
|
393
422
|
:param filename: output file name
|
|
394
423
|
"""
|
|
395
|
-
import wave
|
|
396
424
|
# Define the parameters for the WAV file
|
|
397
425
|
channels = self.actual_audio_format.channelCount()
|
|
398
|
-
sample_size = self.actual_audio_format.bytesPerSample()
|
|
399
426
|
frame_rate = self.actual_audio_format.sampleRate()
|
|
427
|
+
sample_format = self.actual_audio_format.sampleFormat()
|
|
428
|
+
|
|
429
|
+
raw = b''.join(self.frames)
|
|
430
|
+
|
|
431
|
+
if sample_format == QAudioFormat.SampleFormat.Int16:
|
|
432
|
+
out_bytes = raw
|
|
433
|
+
sample_size = 2
|
|
434
|
+
elif sample_format == QAudioFormat.SampleFormat.UInt8:
|
|
435
|
+
arr = np.frombuffer(raw, dtype=np.uint8).astype(np.int16)
|
|
436
|
+
arr = (arr - 128) << 8
|
|
437
|
+
out_bytes = arr.tobytes()
|
|
438
|
+
sample_size = 2
|
|
439
|
+
elif sample_format == QAudioFormat.SampleFormat.Int32:
|
|
440
|
+
arr = np.frombuffer(raw, dtype=np.int32)
|
|
441
|
+
arr = (arr >> 16).astype(np.int16)
|
|
442
|
+
out_bytes = arr.tobytes()
|
|
443
|
+
sample_size = 2
|
|
444
|
+
elif sample_format == QAudioFormat.SampleFormat.Float:
|
|
445
|
+
arr = np.frombuffer(raw, dtype=np.float32)
|
|
446
|
+
arr = np.clip(arr, -1.0, 1.0)
|
|
447
|
+
arr = (arr * 32767.0).astype(np.int16)
|
|
448
|
+
out_bytes = arr.tobytes()
|
|
449
|
+
sample_size = 2
|
|
450
|
+
else:
|
|
451
|
+
raise ValueError("Unsupported sample format")
|
|
400
452
|
|
|
401
|
-
# Open the WAV file
|
|
402
453
|
wf = wave.open(filename, 'wb')
|
|
403
454
|
wf.setnchannels(channels)
|
|
404
455
|
wf.setsampwidth(sample_size)
|
|
405
456
|
wf.setframerate(frame_rate)
|
|
406
|
-
|
|
407
|
-
# Write frames to the file
|
|
408
|
-
wf.writeframes(b''.join(self.frames))
|
|
457
|
+
wf.writeframes(out_bytes)
|
|
409
458
|
wf.close()
|
|
410
459
|
|
|
411
460
|
def get_dtype_from_sample_format(self, sample_format):
|
|
@@ -414,9 +463,6 @@ class NativeBackend(QObject):
|
|
|
414
463
|
|
|
415
464
|
:param sample_format: QAudioFormat.SampleFormat
|
|
416
465
|
"""
|
|
417
|
-
import numpy as np
|
|
418
|
-
from PySide6.QtMultimedia import QAudioFormat
|
|
419
|
-
|
|
420
466
|
if sample_format == QAudioFormat.SampleFormat.UInt8:
|
|
421
467
|
return np.uint8
|
|
422
468
|
elif sample_format == QAudioFormat.SampleFormat.Int16:
|
|
@@ -434,8 +480,6 @@ class NativeBackend(QObject):
|
|
|
434
480
|
|
|
435
481
|
:param sample_format: QAudioFormat.SampleFormat
|
|
436
482
|
"""
|
|
437
|
-
from PySide6.QtMultimedia import QAudioFormat
|
|
438
|
-
|
|
439
483
|
if sample_format == QAudioFormat.SampleFormat.UInt8:
|
|
440
484
|
return 255.0
|
|
441
485
|
elif sample_format == QAudioFormat.SampleFormat.Int16:
|
|
@@ -463,11 +507,6 @@ class NativeBackend(QObject):
|
|
|
463
507
|
:param signals: Signals to emit on playback
|
|
464
508
|
:return: True if started
|
|
465
509
|
"""
|
|
466
|
-
from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput, QMediaDevices
|
|
467
|
-
from PySide6.QtCore import QUrl, QTimer
|
|
468
|
-
if signals is not None:
|
|
469
|
-
signals.playback.emit(event_name)
|
|
470
|
-
|
|
471
510
|
self.audio_output = QAudioOutput()
|
|
472
511
|
self.audio_output.setVolume(1.0)
|
|
473
512
|
|
|
@@ -519,6 +558,7 @@ class NativeBackend(QObject):
|
|
|
519
558
|
self.playback_timer.start()
|
|
520
559
|
self.volume_timer.start()
|
|
521
560
|
signals.volume_changed.emit(0)
|
|
561
|
+
signals.playback.emit(event_name)
|
|
522
562
|
|
|
523
563
|
def stop_timers(self):
|
|
524
564
|
"""
|
|
@@ -558,8 +598,7 @@ class NativeBackend(QObject):
|
|
|
558
598
|
"""
|
|
559
599
|
if self.player is not None:
|
|
560
600
|
self.player.stop()
|
|
561
|
-
|
|
562
|
-
self.playback_timer.stop()
|
|
601
|
+
self.stop_timers()
|
|
563
602
|
return False
|
|
564
603
|
|
|
565
604
|
def calculate_envelope(
|
|
@@ -573,7 +612,6 @@ class NativeBackend(QObject):
|
|
|
573
612
|
:param audio_file: Path to the audio file
|
|
574
613
|
:param chunk_ms: Size of each chunk in milliseconds
|
|
575
614
|
"""
|
|
576
|
-
import numpy as np
|
|
577
615
|
audio = AudioSegment.from_file(audio_file)
|
|
578
616
|
max_amplitude = 32767
|
|
579
617
|
envelope = []
|
|
@@ -611,8 +649,6 @@ class NativeBackend(QObject):
|
|
|
611
649
|
|
|
612
650
|
:return devices list: [(id, name)]
|
|
613
651
|
"""
|
|
614
|
-
from bs4 import UnicodeDammit
|
|
615
|
-
from PySide6.QtMultimedia import QMediaDevices
|
|
616
652
|
devices = QMediaDevices.audioInputs()
|
|
617
653
|
devices_list = []
|
|
618
654
|
for index, device in enumerate(devices):
|
|
@@ -626,8 +662,6 @@ class NativeBackend(QObject):
|
|
|
626
662
|
|
|
627
663
|
:return devices list: [(id, name)]
|
|
628
664
|
"""
|
|
629
|
-
from bs4 import UnicodeDammit
|
|
630
|
-
from PySide6.QtMultimedia import QMediaDevices
|
|
631
665
|
devices = QMediaDevices.audioOutputs()
|
|
632
666
|
devices_list = []
|
|
633
667
|
for index, device in enumerate(devices):
|
|
@@ -641,7 +675,6 @@ class NativeBackend(QObject):
|
|
|
641
675
|
|
|
642
676
|
:return: (device_id, error_message) tuple
|
|
643
677
|
"""
|
|
644
|
-
from PySide6.QtMultimedia import QMediaDevices
|
|
645
678
|
default_device = QMediaDevices.defaultAudioInput()
|
|
646
679
|
devices = QMediaDevices.audioInputs()
|
|
647
680
|
try:
|
|
@@ -656,11 +689,10 @@ class NativeBackend(QObject):
|
|
|
656
689
|
|
|
657
690
|
:return: (device_id, None) if successful, (None, error_message) if failed
|
|
658
691
|
"""
|
|
659
|
-
from PySide6.QtMultimedia import QMediaDevices
|
|
660
692
|
default_device = QMediaDevices.defaultAudioOutput()
|
|
661
693
|
devices = QMediaDevices.audioOutputs()
|
|
662
694
|
try:
|
|
663
695
|
index = devices.index(default_device)
|
|
664
696
|
except ValueError:
|
|
665
697
|
index = None
|
|
666
|
-
return index, None
|
|
698
|
+
return index, None
|
|
@@ -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.27 07:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import time
|
|
@@ -27,7 +27,6 @@ class PyaudioBackend:
|
|
|
27
27
|
self.window = window
|
|
28
28
|
self.path = None
|
|
29
29
|
self.frames = []
|
|
30
|
-
self.bar = None
|
|
31
30
|
self.loop = False
|
|
32
31
|
self.stop_callback = None
|
|
33
32
|
self.start_time = 0
|
|
@@ -36,6 +35,7 @@ class PyaudioBackend:
|
|
|
36
35
|
self.pyaudio_instance_output = None
|
|
37
36
|
self.stream = None
|
|
38
37
|
self.stream_output = None
|
|
38
|
+
self.mode = "input" # input|control
|
|
39
39
|
|
|
40
40
|
# Get configuration values (use defaults if unavailable)
|
|
41
41
|
if self.window is not None and hasattr(self.window, "core"):
|
|
@@ -60,6 +60,14 @@ class PyaudioBackend:
|
|
|
60
60
|
self.check_audio_devices()
|
|
61
61
|
self.initialized = True
|
|
62
62
|
|
|
63
|
+
def set_mode(self, mode: str):
|
|
64
|
+
"""
|
|
65
|
+
Set input mode (input|control)
|
|
66
|
+
|
|
67
|
+
:param mode: mode name
|
|
68
|
+
"""
|
|
69
|
+
self.mode = mode
|
|
70
|
+
|
|
63
71
|
def set_repeat_callback(self, callback):
|
|
64
72
|
"""
|
|
65
73
|
Set callback to be called on loop recording.
|
|
@@ -87,14 +95,6 @@ class PyaudioBackend:
|
|
|
87
95
|
"""
|
|
88
96
|
self.path = path
|
|
89
97
|
|
|
90
|
-
def set_bar(self, bar):
|
|
91
|
-
"""
|
|
92
|
-
Set audio level bar.
|
|
93
|
-
|
|
94
|
-
:param bar: audio level bar widget to update with audio levels
|
|
95
|
-
"""
|
|
96
|
-
self.bar = bar
|
|
97
|
-
|
|
98
98
|
def start(self):
|
|
99
99
|
"""
|
|
100
100
|
Start audio input recording using PyAudio.
|
|
@@ -169,8 +169,7 @@ class PyaudioBackend:
|
|
|
169
169
|
"""
|
|
170
170
|
Reset the audio level bar.
|
|
171
171
|
"""
|
|
172
|
-
|
|
173
|
-
self.bar.setLevel(0)
|
|
172
|
+
self.window.controller.audio.ui.on_input_volume_change(0, self.mode)
|
|
174
173
|
|
|
175
174
|
def check_audio_input(self) -> bool:
|
|
176
175
|
"""
|
|
@@ -284,11 +283,10 @@ class PyaudioBackend:
|
|
|
284
283
|
level_percent = int(level * 100)
|
|
285
284
|
|
|
286
285
|
# Update the audio level bar if available.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
pass
|
|
286
|
+
try:
|
|
287
|
+
QTimer.singleShot(0, lambda: self.window.controller.audio.ui.on_input_volume_change(level_percent, self.mode))
|
|
288
|
+
except Exception:
|
|
289
|
+
pass
|
|
292
290
|
|
|
293
291
|
# Handle loop recording if enabled.
|
|
294
292
|
if self.loop and self.stop_callback is not None:
|
|
@@ -306,8 +304,7 @@ class PyaudioBackend:
|
|
|
306
304
|
Update the audio level bar.
|
|
307
305
|
:param level: audio level (0-100).
|
|
308
306
|
"""
|
|
309
|
-
|
|
310
|
-
self.bar.setLevel(level)
|
|
307
|
+
self.window.controller.audio.ui.on_input_volume_change(level, self.mode)
|
|
311
308
|
|
|
312
309
|
def save_audio_file(self, filename: str):
|
|
313
310
|
"""
|
|
@@ -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.27 07:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import time
|
|
@@ -26,7 +26,6 @@ class PygameBackend:
|
|
|
26
26
|
self.window = window
|
|
27
27
|
self.path = None
|
|
28
28
|
self.frames = [] # List to store captured audio chunks (bytes)
|
|
29
|
-
self.bar = None # Optional audio level bar widget
|
|
30
29
|
self.loop = False
|
|
31
30
|
self.stop_callback = None
|
|
32
31
|
self.start_time = 0
|
|
@@ -54,6 +53,7 @@ class PygameBackend:
|
|
|
54
53
|
self.devices = []
|
|
55
54
|
self.selected_device = None
|
|
56
55
|
self.initialized = False
|
|
56
|
+
self.mode = "input" # input|control
|
|
57
57
|
|
|
58
58
|
def init(self):
|
|
59
59
|
"""
|
|
@@ -75,6 +75,14 @@ class PygameBackend:
|
|
|
75
75
|
self.check_audio_devices()
|
|
76
76
|
self.initialized = True
|
|
77
77
|
|
|
78
|
+
def set_mode(self, mode: str):
|
|
79
|
+
"""
|
|
80
|
+
Set input mode (input|control)
|
|
81
|
+
|
|
82
|
+
:param mode: mode name
|
|
83
|
+
"""
|
|
84
|
+
self.mode = mode
|
|
85
|
+
|
|
78
86
|
def set_repeat_callback(self, callback):
|
|
79
87
|
"""
|
|
80
88
|
Set a callback to be called on loop recording.
|
|
@@ -102,15 +110,6 @@ class PygameBackend:
|
|
|
102
110
|
"""
|
|
103
111
|
self.path = path
|
|
104
112
|
|
|
105
|
-
def set_bar(self, bar):
|
|
106
|
-
"""
|
|
107
|
-
Set an audio level bar widget.
|
|
108
|
-
If not available, this will be silently ignored.
|
|
109
|
-
|
|
110
|
-
:param bar: A widget that has a setLevel method to update the audio level.
|
|
111
|
-
"""
|
|
112
|
-
self.bar = bar
|
|
113
|
-
|
|
114
113
|
def start(self):
|
|
115
114
|
"""
|
|
116
115
|
Start audio recording using pygame’s SDL2 audio capture.
|
|
@@ -187,8 +186,7 @@ class PygameBackend:
|
|
|
187
186
|
"""
|
|
188
187
|
Reset the audio level bar (if available).
|
|
189
188
|
"""
|
|
190
|
-
|
|
191
|
-
self.bar.setLevel(0)
|
|
189
|
+
self.window.controller.audio.ui.on_input_volume_change(0, self.mode)
|
|
192
190
|
|
|
193
191
|
def check_audio_input(self) -> bool:
|
|
194
192
|
"""
|
|
@@ -312,8 +310,7 @@ class PygameBackend:
|
|
|
312
310
|
level = min(max(rms, 0.0), 1.0)
|
|
313
311
|
level_percent = int(level * 100)
|
|
314
312
|
|
|
315
|
-
|
|
316
|
-
QTimer.singleShot(0, lambda: self.bar.setLevel(level_percent))
|
|
313
|
+
QTimer.singleShot(0, lambda: self.window.controller.audio.ui.on_input_volume_change(level_percent, self.mode))
|
|
317
314
|
|
|
318
315
|
# Handle loop recording if enabled.
|
|
319
316
|
if self.loop and self.stop_callback is not None:
|
pygpt_net/core/audio/capture.py
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
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.27 07:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
|
+
|
|
11
12
|
from typing import List, Tuple
|
|
12
13
|
|
|
13
14
|
from .backend.native import NativeBackend
|
|
@@ -57,6 +58,14 @@ class Capture:
|
|
|
57
58
|
"""
|
|
58
59
|
self.get_backend().set_repeat_callback(callback)
|
|
59
60
|
|
|
61
|
+
def set_mode(self, mode: str):
|
|
62
|
+
"""
|
|
63
|
+
Set input mode (input|control)
|
|
64
|
+
|
|
65
|
+
:param mode: mode name
|
|
66
|
+
"""
|
|
67
|
+
self.get_backend().set_mode(mode)
|
|
68
|
+
|
|
60
69
|
def set_loop(self, loop: bool):
|
|
61
70
|
"""
|
|
62
71
|
Set loop recording
|
|
@@ -73,14 +82,6 @@ class Capture:
|
|
|
73
82
|
"""
|
|
74
83
|
self.get_backend().set_path(path)
|
|
75
84
|
|
|
76
|
-
def set_bar(self, bar):
|
|
77
|
-
"""
|
|
78
|
-
Set audio level bar
|
|
79
|
-
|
|
80
|
-
:param bar: audio level bar
|
|
81
|
-
"""
|
|
82
|
-
self.get_backend().set_bar(bar)
|
|
83
|
-
|
|
84
85
|
def start(self):
|
|
85
86
|
"""
|
|
86
87
|
Start audio input recording
|
pygpt_net/core/audio/context.py
CHANGED
|
@@ -6,12 +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:
|
|
9
|
+
# Updated Date: 2025.08.27 07:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
-
from typing import Dict
|
|
13
|
-
|
|
14
|
-
from pygpt_net.item.ctx import CtxItem
|
|
12
|
+
from typing import Dict
|
|
15
13
|
|
|
16
14
|
|
|
17
15
|
class AudioContext:
|
|
@@ -35,5 +33,4 @@ class AudioContext:
|
|
|
35
33
|
"prev_id": self.prev_id
|
|
36
34
|
}
|
|
37
35
|
# sort by keys
|
|
38
|
-
|
|
39
|
-
return data
|
|
36
|
+
return dict(sorted(data.items(), key=lambda item: item[0]))
|