pygpt-net 2.6.30__py3-none-any.whl → 2.6.32__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 +15 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +7 -1
- pygpt_net/app_core.py +3 -1
- pygpt_net/config.py +3 -1
- pygpt_net/controller/__init__.py +9 -2
- pygpt_net/controller/audio/audio.py +38 -1
- pygpt_net/controller/audio/ui.py +2 -2
- pygpt_net/controller/chat/audio.py +1 -8
- pygpt_net/controller/chat/common.py +23 -62
- pygpt_net/controller/chat/handler/__init__.py +0 -0
- pygpt_net/controller/chat/handler/stream_worker.py +1124 -0
- pygpt_net/controller/chat/output.py +8 -3
- pygpt_net/controller/chat/stream.py +3 -1071
- pygpt_net/controller/chat/text.py +3 -2
- pygpt_net/controller/kernel/kernel.py +11 -3
- pygpt_net/controller/kernel/reply.py +5 -1
- pygpt_net/controller/lang/custom.py +2 -2
- pygpt_net/controller/media/__init__.py +12 -0
- pygpt_net/controller/media/media.py +115 -0
- pygpt_net/controller/realtime/__init__.py +12 -0
- pygpt_net/controller/realtime/manager.py +53 -0
- pygpt_net/controller/realtime/realtime.py +293 -0
- pygpt_net/controller/ui/mode.py +23 -2
- pygpt_net/controller/ui/ui.py +19 -1
- pygpt_net/core/audio/audio.py +6 -1
- pygpt_net/core/audio/backend/native/__init__.py +12 -0
- pygpt_net/core/audio/backend/{native.py → native/native.py} +426 -127
- pygpt_net/core/audio/backend/native/player.py +139 -0
- pygpt_net/core/audio/backend/native/realtime.py +250 -0
- pygpt_net/core/audio/backend/pyaudio/__init__.py +12 -0
- pygpt_net/core/audio/backend/pyaudio/playback.py +194 -0
- pygpt_net/core/audio/backend/pyaudio/pyaudio.py +923 -0
- pygpt_net/core/audio/backend/pyaudio/realtime.py +312 -0
- pygpt_net/core/audio/backend/pygame/__init__.py +12 -0
- pygpt_net/core/audio/backend/{pygame.py → pygame/pygame.py} +130 -19
- pygpt_net/core/audio/backend/shared/__init__.py +38 -0
- pygpt_net/core/audio/backend/shared/conversions.py +211 -0
- pygpt_net/core/audio/backend/shared/envelope.py +38 -0
- pygpt_net/core/audio/backend/shared/player.py +137 -0
- pygpt_net/core/audio/backend/shared/rt.py +52 -0
- pygpt_net/core/audio/capture.py +5 -0
- pygpt_net/core/audio/output.py +14 -2
- pygpt_net/core/audio/whisper.py +6 -2
- pygpt_net/core/bridge/bridge.py +2 -1
- pygpt_net/core/bridge/worker.py +4 -1
- pygpt_net/core/dispatcher/dispatcher.py +37 -1
- pygpt_net/core/events/__init__.py +2 -1
- pygpt_net/core/events/realtime.py +55 -0
- pygpt_net/core/image/image.py +56 -5
- pygpt_net/core/realtime/__init__.py +0 -0
- pygpt_net/core/realtime/options.py +87 -0
- pygpt_net/core/realtime/shared/__init__.py +0 -0
- pygpt_net/core/realtime/shared/audio.py +213 -0
- pygpt_net/core/realtime/shared/loop.py +64 -0
- pygpt_net/core/realtime/shared/session.py +59 -0
- pygpt_net/core/realtime/shared/text.py +37 -0
- pygpt_net/core/realtime/shared/tools.py +276 -0
- pygpt_net/core/realtime/shared/turn.py +38 -0
- pygpt_net/core/realtime/shared/types.py +16 -0
- pygpt_net/core/realtime/worker.py +160 -0
- pygpt_net/core/render/web/body.py +24 -3
- pygpt_net/core/text/utils.py +54 -2
- pygpt_net/core/types/__init__.py +1 -0
- pygpt_net/core/types/image.py +54 -0
- pygpt_net/core/video/__init__.py +12 -0
- pygpt_net/core/video/video.py +290 -0
- pygpt_net/data/config/config.json +26 -5
- pygpt_net/data/config/models.json +221 -103
- pygpt_net/data/config/settings.json +244 -6
- pygpt_net/data/css/web-blocks.css +6 -0
- pygpt_net/data/css/web-chatgpt.css +6 -0
- pygpt_net/data/css/web-chatgpt_wide.css +6 -0
- pygpt_net/data/locale/locale.de.ini +35 -7
- pygpt_net/data/locale/locale.en.ini +56 -17
- pygpt_net/data/locale/locale.es.ini +35 -7
- pygpt_net/data/locale/locale.fr.ini +35 -7
- pygpt_net/data/locale/locale.it.ini +35 -7
- pygpt_net/data/locale/locale.pl.ini +38 -7
- pygpt_net/data/locale/locale.uk.ini +35 -7
- pygpt_net/data/locale/locale.zh.ini +31 -3
- pygpt_net/data/locale/plugin.audio_input.en.ini +4 -0
- pygpt_net/data/locale/plugin.audio_output.en.ini +4 -0
- pygpt_net/data/locale/plugin.cmd_web.en.ini +8 -0
- pygpt_net/item/model.py +22 -1
- pygpt_net/plugin/audio_input/plugin.py +37 -4
- pygpt_net/plugin/audio_input/simple.py +57 -8
- pygpt_net/plugin/cmd_files/worker.py +3 -0
- pygpt_net/provider/api/google/__init__.py +76 -7
- pygpt_net/provider/api/google/audio.py +8 -1
- pygpt_net/provider/api/google/chat.py +45 -6
- pygpt_net/provider/api/google/image.py +226 -86
- pygpt_net/provider/api/google/realtime/__init__.py +12 -0
- pygpt_net/provider/api/google/realtime/client.py +1945 -0
- pygpt_net/provider/api/google/realtime/realtime.py +186 -0
- pygpt_net/provider/api/google/video.py +364 -0
- pygpt_net/provider/api/openai/__init__.py +22 -2
- pygpt_net/provider/api/openai/realtime/__init__.py +12 -0
- pygpt_net/provider/api/openai/realtime/client.py +1828 -0
- pygpt_net/provider/api/openai/realtime/realtime.py +193 -0
- pygpt_net/provider/audio_input/google_genai.py +103 -0
- pygpt_net/provider/audio_output/google_genai_tts.py +229 -0
- pygpt_net/provider/audio_output/google_tts.py +0 -12
- pygpt_net/provider/audio_output/openai_tts.py +8 -5
- pygpt_net/provider/core/config/patch.py +241 -178
- pygpt_net/provider/core/model/patch.py +28 -2
- pygpt_net/provider/llms/google.py +8 -9
- pygpt_net/provider/web/duckduck_search.py +212 -0
- pygpt_net/ui/layout/toolbox/audio.py +55 -0
- pygpt_net/ui/layout/toolbox/footer.py +14 -42
- pygpt_net/ui/layout/toolbox/image.py +7 -13
- pygpt_net/ui/layout/toolbox/raw.py +52 -0
- pygpt_net/ui/layout/toolbox/split.py +48 -0
- pygpt_net/ui/layout/toolbox/toolbox.py +8 -8
- pygpt_net/ui/layout/toolbox/video.py +49 -0
- pygpt_net/ui/widget/option/combo.py +15 -1
- {pygpt_net-2.6.30.dist-info → pygpt_net-2.6.32.dist-info}/METADATA +46 -22
- {pygpt_net-2.6.30.dist-info → pygpt_net-2.6.32.dist-info}/RECORD +121 -73
- pygpt_net/core/audio/backend/pyaudio.py +0 -554
- {pygpt_net-2.6.30.dist-info → pygpt_net-2.6.32.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.30.dist-info → pygpt_net-2.6.32.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.30.dist-info → pygpt_net-2.6.32.dist-info}/entry_points.txt +0 -0
|
@@ -1,554 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
# ================================================== #
|
|
4
|
-
# This file is a part of PYGPT package #
|
|
5
|
-
# Website: https://pygpt.net #
|
|
6
|
-
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
|
-
# MIT License #
|
|
8
|
-
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date: 2025.08.27 07:00:00 #
|
|
10
|
-
# ================================================== #
|
|
11
|
-
|
|
12
|
-
import time
|
|
13
|
-
from typing import List, Tuple
|
|
14
|
-
|
|
15
|
-
import wave
|
|
16
|
-
from PySide6.QtCore import QTimer
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class PyaudioBackend:
|
|
20
|
-
|
|
21
|
-
MIN_FRAMES = 25 # minimum frames to start transcription
|
|
22
|
-
|
|
23
|
-
def __init__(self, window=None):
|
|
24
|
-
"""
|
|
25
|
-
Audio input capture core using PyAudio backend
|
|
26
|
-
"""
|
|
27
|
-
self.window = window
|
|
28
|
-
self.path = None
|
|
29
|
-
self.frames = []
|
|
30
|
-
self.loop = False
|
|
31
|
-
self.stop_callback = None
|
|
32
|
-
self.start_time = 0
|
|
33
|
-
self.initialized = False
|
|
34
|
-
self.pyaudio_instance = None
|
|
35
|
-
self.pyaudio_instance_output = None
|
|
36
|
-
self.stream = None
|
|
37
|
-
self.stream_output = None
|
|
38
|
-
self.mode = "input" # input|control
|
|
39
|
-
|
|
40
|
-
# Get configuration values (use defaults if unavailable)
|
|
41
|
-
if self.window is not None and hasattr(self.window, "core"):
|
|
42
|
-
self.channels = int(self.window.core.config.get('audio.input.channels', 1))
|
|
43
|
-
self.rate = int(self.window.core.config.get('audio.input.rate', 44100))
|
|
44
|
-
else:
|
|
45
|
-
self.channels = 1
|
|
46
|
-
self.rate = 44100
|
|
47
|
-
|
|
48
|
-
self.format = None
|
|
49
|
-
self.devices = []
|
|
50
|
-
self.selected_device = None
|
|
51
|
-
|
|
52
|
-
def init(self):
|
|
53
|
-
"""
|
|
54
|
-
Initialize audio input backend.
|
|
55
|
-
"""
|
|
56
|
-
import pyaudio
|
|
57
|
-
if not self.initialized:
|
|
58
|
-
self.format = pyaudio.paInt16 # We use paInt16 as default format
|
|
59
|
-
self.pyaudio_instance = pyaudio.PyAudio()
|
|
60
|
-
self.check_audio_devices()
|
|
61
|
-
self.initialized = True
|
|
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
|
-
|
|
71
|
-
def set_repeat_callback(self, callback):
|
|
72
|
-
"""
|
|
73
|
-
Set callback to be called on loop recording.
|
|
74
|
-
|
|
75
|
-
:param callback: function to call on loop recording
|
|
76
|
-
"""
|
|
77
|
-
if callable(callback):
|
|
78
|
-
self.stop_callback = callback
|
|
79
|
-
else:
|
|
80
|
-
raise ValueError("Callback must be a callable function")
|
|
81
|
-
|
|
82
|
-
def set_loop(self, loop: bool):
|
|
83
|
-
"""
|
|
84
|
-
Set loop recording.
|
|
85
|
-
|
|
86
|
-
:param loop: True to enable loop recording
|
|
87
|
-
"""
|
|
88
|
-
self.loop = loop
|
|
89
|
-
|
|
90
|
-
def set_path(self, path: str):
|
|
91
|
-
"""
|
|
92
|
-
Set audio input file path.
|
|
93
|
-
|
|
94
|
-
:param path: file path to save recorded audio
|
|
95
|
-
"""
|
|
96
|
-
self.path = path
|
|
97
|
-
|
|
98
|
-
def start(self):
|
|
99
|
-
"""
|
|
100
|
-
Start audio input recording using PyAudio.
|
|
101
|
-
:return: True if started
|
|
102
|
-
"""
|
|
103
|
-
self.init()
|
|
104
|
-
|
|
105
|
-
# Clear previous frames
|
|
106
|
-
self.frames = []
|
|
107
|
-
|
|
108
|
-
# Prepare selected device from configuration
|
|
109
|
-
self.prepare_device()
|
|
110
|
-
if self.selected_device is None:
|
|
111
|
-
print("No audio input device selected")
|
|
112
|
-
return False
|
|
113
|
-
|
|
114
|
-
# Prevent multiple recordings
|
|
115
|
-
if self.stream is not None:
|
|
116
|
-
return False
|
|
117
|
-
|
|
118
|
-
# Set up audio input and start recording
|
|
119
|
-
self.setup_audio_input()
|
|
120
|
-
self.start_time = time.time()
|
|
121
|
-
return True
|
|
122
|
-
|
|
123
|
-
def stop(self) -> bool:
|
|
124
|
-
"""
|
|
125
|
-
Stop audio input recording.
|
|
126
|
-
:return: True if stopped (and file saved) or False otherwise.
|
|
127
|
-
"""
|
|
128
|
-
result = False
|
|
129
|
-
if self.stream is not None:
|
|
130
|
-
try:
|
|
131
|
-
self.stream.stop_stream()
|
|
132
|
-
self.stream.close()
|
|
133
|
-
except Exception as e:
|
|
134
|
-
print(f"Error stopping stream: {e}")
|
|
135
|
-
self.stream = None
|
|
136
|
-
|
|
137
|
-
if self.frames:
|
|
138
|
-
if self.path:
|
|
139
|
-
self.save_audio_file(self.path)
|
|
140
|
-
result = True
|
|
141
|
-
else:
|
|
142
|
-
print("File path is not set.")
|
|
143
|
-
else:
|
|
144
|
-
print("No audio data recorded")
|
|
145
|
-
return result
|
|
146
|
-
|
|
147
|
-
def has_source(self) -> bool:
|
|
148
|
-
"""
|
|
149
|
-
Check if audio source is available.
|
|
150
|
-
:return: True if available.
|
|
151
|
-
"""
|
|
152
|
-
return self.stream is not None
|
|
153
|
-
|
|
154
|
-
def has_frames(self) -> bool:
|
|
155
|
-
"""
|
|
156
|
-
Check if audio frames are available.
|
|
157
|
-
:return: True if available.
|
|
158
|
-
"""
|
|
159
|
-
return bool(self.frames)
|
|
160
|
-
|
|
161
|
-
def has_min_frames(self) -> bool:
|
|
162
|
-
"""
|
|
163
|
-
Check if minimum required audio frames have been recorded.
|
|
164
|
-
:return: True if min frames reached.
|
|
165
|
-
"""
|
|
166
|
-
return len(self.frames) >= self.MIN_FRAMES
|
|
167
|
-
|
|
168
|
-
def reset_audio_level(self):
|
|
169
|
-
"""
|
|
170
|
-
Reset the audio level bar.
|
|
171
|
-
"""
|
|
172
|
-
self.window.controller.audio.ui.on_input_volume_change(0, self.mode)
|
|
173
|
-
|
|
174
|
-
def check_audio_input(self) -> bool:
|
|
175
|
-
"""
|
|
176
|
-
Check if default audio input device is working using PyAudio.
|
|
177
|
-
:return: True if working.
|
|
178
|
-
"""
|
|
179
|
-
self.init()
|
|
180
|
-
try:
|
|
181
|
-
test_stream = self.pyaudio_instance.open(format=self.format,
|
|
182
|
-
channels=self.channels,
|
|
183
|
-
rate=self.rate,
|
|
184
|
-
input=True,
|
|
185
|
-
frames_per_buffer=1024)
|
|
186
|
-
test_stream.stop_stream()
|
|
187
|
-
test_stream.close()
|
|
188
|
-
return True
|
|
189
|
-
except Exception as e:
|
|
190
|
-
return False
|
|
191
|
-
|
|
192
|
-
def check_audio_devices(self):
|
|
193
|
-
"""
|
|
194
|
-
Check audio input devices using PyAudio and populate self.devices.
|
|
195
|
-
Each device is stored as a dict with keys 'index' and 'name'.
|
|
196
|
-
"""
|
|
197
|
-
self.devices = []
|
|
198
|
-
for i in range(self.pyaudio_instance.get_device_count()):
|
|
199
|
-
try:
|
|
200
|
-
info = self.pyaudio_instance.get_device_info_by_index(i)
|
|
201
|
-
if info.get('maxInputChannels', 0) > 0:
|
|
202
|
-
self.devices.append({'index': i, 'name': info.get('name', f'Device {i}')})
|
|
203
|
-
except Exception as e:
|
|
204
|
-
continue
|
|
205
|
-
|
|
206
|
-
if not self.devices:
|
|
207
|
-
self.selected_device = None
|
|
208
|
-
print("No audio input devices found.")
|
|
209
|
-
else:
|
|
210
|
-
# Set the first available device as default
|
|
211
|
-
self.selected_device = self.devices[0]['index']
|
|
212
|
-
|
|
213
|
-
def device_changed(self, index: int):
|
|
214
|
-
"""
|
|
215
|
-
Change audio input device based on device list index.
|
|
216
|
-
:param index: device list index.
|
|
217
|
-
"""
|
|
218
|
-
self.init()
|
|
219
|
-
if 0 <= index < len(self.devices):
|
|
220
|
-
self.selected_device = self.devices[index]['index']
|
|
221
|
-
else:
|
|
222
|
-
self.selected_device = 0
|
|
223
|
-
|
|
224
|
-
def prepare_device(self):
|
|
225
|
-
"""
|
|
226
|
-
Set the current audio input device from configuration.
|
|
227
|
-
"""
|
|
228
|
-
self.init()
|
|
229
|
-
if self.window is not None and hasattr(self.window, "core"):
|
|
230
|
-
device_id = int(self.window.core.config.get('audio.input.device', 0))
|
|
231
|
-
self.device_changed(device_id)
|
|
232
|
-
else:
|
|
233
|
-
# Default to first available device
|
|
234
|
-
if self.devices:
|
|
235
|
-
print(self.devices)
|
|
236
|
-
self.selected_device = self.devices[0]['index']
|
|
237
|
-
else:
|
|
238
|
-
self.selected_device = None
|
|
239
|
-
|
|
240
|
-
def setup_audio_input(self):
|
|
241
|
-
"""
|
|
242
|
-
Set up audio input device and start recording using PyAudio.
|
|
243
|
-
"""
|
|
244
|
-
self.init()
|
|
245
|
-
if self.selected_device is None:
|
|
246
|
-
print("No audio input device selected")
|
|
247
|
-
return
|
|
248
|
-
|
|
249
|
-
print("Opening audio stream with device index:", self.selected_device)
|
|
250
|
-
try:
|
|
251
|
-
self.stream = self.pyaudio_instance.open(format=self.format,
|
|
252
|
-
channels=self.channels,
|
|
253
|
-
rate=self.rate,
|
|
254
|
-
input=True,
|
|
255
|
-
frames_per_buffer=1024,
|
|
256
|
-
stream_callback=self._audio_callback)
|
|
257
|
-
except Exception as e:
|
|
258
|
-
print(f"Failed to open audio stream: {e}")
|
|
259
|
-
self.stream = None
|
|
260
|
-
|
|
261
|
-
def _audio_callback(self, in_data, frame_count, time_info, status):
|
|
262
|
-
"""
|
|
263
|
-
PyAudio callback to process incoming audio data.
|
|
264
|
-
"""
|
|
265
|
-
import pyaudio
|
|
266
|
-
import numpy as np
|
|
267
|
-
|
|
268
|
-
# Append raw data to the frames list for saving
|
|
269
|
-
self.frames.append(in_data)
|
|
270
|
-
|
|
271
|
-
# Convert bytes data to a NumPy array using the correct data type
|
|
272
|
-
dtype = self.get_dtype_from_format(self.format)
|
|
273
|
-
samples = np.frombuffer(in_data, dtype=dtype)
|
|
274
|
-
if samples.size == 0:
|
|
275
|
-
return None, pyaudio.paContinue
|
|
276
|
-
|
|
277
|
-
# Compute root mean square (RMS) for the audio samples
|
|
278
|
-
rms = np.sqrt(np.mean(samples.astype(np.float64) ** 2))
|
|
279
|
-
normalization_factor = self.get_normalization_factor(self.format)
|
|
280
|
-
level = rms / normalization_factor
|
|
281
|
-
# Clamp level to 0.0 - 1.0 range
|
|
282
|
-
level = min(max(level, 0.0), 1.0)
|
|
283
|
-
level_percent = int(level * 100)
|
|
284
|
-
|
|
285
|
-
# Update the audio level bar if available.
|
|
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
|
|
290
|
-
|
|
291
|
-
# Handle loop recording if enabled.
|
|
292
|
-
if self.loop and self.stop_callback is not None:
|
|
293
|
-
stop_interval = int(self.window.core.config.get('audio.input.stop_interval', 10)) \
|
|
294
|
-
if self.window and hasattr(self.window, "core") else 10
|
|
295
|
-
current_time = time.time()
|
|
296
|
-
if current_time - self.start_time >= stop_interval:
|
|
297
|
-
self.start_time = current_time
|
|
298
|
-
QTimer.singleShot(0, self.stop_callback)
|
|
299
|
-
|
|
300
|
-
return None, pyaudio.paContinue
|
|
301
|
-
|
|
302
|
-
def update_audio_level(self, level: int):
|
|
303
|
-
"""
|
|
304
|
-
Update the audio level bar.
|
|
305
|
-
:param level: audio level (0-100).
|
|
306
|
-
"""
|
|
307
|
-
self.window.controller.audio.ui.on_input_volume_change(level, self.mode)
|
|
308
|
-
|
|
309
|
-
def save_audio_file(self, filename: str):
|
|
310
|
-
"""
|
|
311
|
-
Save the recorded audio frames to a WAV file.
|
|
312
|
-
:param filename: output file name.
|
|
313
|
-
"""
|
|
314
|
-
sample_width = self.pyaudio_instance.get_sample_size(self.format)
|
|
315
|
-
with wave.open(filename, 'wb') as wf:
|
|
316
|
-
wf.setnchannels(self.channels)
|
|
317
|
-
wf.setsampwidth(sample_width)
|
|
318
|
-
wf.setframerate(self.rate)
|
|
319
|
-
wf.writeframes(b''.join(self.frames))
|
|
320
|
-
|
|
321
|
-
def get_dtype_from_format(self, fmt):
|
|
322
|
-
"""
|
|
323
|
-
Get the NumPy dtype corresponding to the PyAudio format.
|
|
324
|
-
:param fmt: PyAudio format constant.
|
|
325
|
-
"""
|
|
326
|
-
import pyaudio
|
|
327
|
-
import numpy as np
|
|
328
|
-
if fmt == pyaudio.paInt16:
|
|
329
|
-
return np.int16
|
|
330
|
-
elif fmt == pyaudio.paInt8:
|
|
331
|
-
return np.int8
|
|
332
|
-
elif fmt == pyaudio.paUInt8:
|
|
333
|
-
return np.uint8
|
|
334
|
-
elif fmt == pyaudio.paFloat32:
|
|
335
|
-
return np.float32
|
|
336
|
-
else:
|
|
337
|
-
raise ValueError("Unsupported audio format")
|
|
338
|
-
|
|
339
|
-
def get_normalization_factor(self, fmt):
|
|
340
|
-
"""
|
|
341
|
-
Get the normalization factor for the given PyAudio format.
|
|
342
|
-
:param fmt: PyAudio format constant.
|
|
343
|
-
"""
|
|
344
|
-
import pyaudio
|
|
345
|
-
if fmt == pyaudio.paInt16:
|
|
346
|
-
return 32768.0
|
|
347
|
-
elif fmt == pyaudio.paInt8:
|
|
348
|
-
return 128.0
|
|
349
|
-
elif fmt == pyaudio.paUInt8:
|
|
350
|
-
return 255.0
|
|
351
|
-
elif fmt == pyaudio.paFloat32:
|
|
352
|
-
return 1.0
|
|
353
|
-
else:
|
|
354
|
-
raise ValueError("Unsupported audio format")
|
|
355
|
-
|
|
356
|
-
def stop_audio(self) -> bool:
|
|
357
|
-
"""
|
|
358
|
-
Stop audio input recording.
|
|
359
|
-
:return: True if stopped.
|
|
360
|
-
"""
|
|
361
|
-
if self.stream is not None:
|
|
362
|
-
try:
|
|
363
|
-
self.stream.stop_stream()
|
|
364
|
-
self.stream.close()
|
|
365
|
-
except Exception as e:
|
|
366
|
-
print(f"Error stopping stream: {e}")
|
|
367
|
-
self.stream = None
|
|
368
|
-
return True
|
|
369
|
-
return False
|
|
370
|
-
|
|
371
|
-
def play(
|
|
372
|
-
self,
|
|
373
|
-
audio_file: str,
|
|
374
|
-
event_name: str,
|
|
375
|
-
stopped: callable,
|
|
376
|
-
signals=None
|
|
377
|
-
):
|
|
378
|
-
"""
|
|
379
|
-
Play audio file using PyAudio
|
|
380
|
-
|
|
381
|
-
:param audio_file: audio file path
|
|
382
|
-
:param event_name: event name to emit when playback starts
|
|
383
|
-
:param stopped: callable to check if playback should stop
|
|
384
|
-
:param signals: signals object to emit playback events
|
|
385
|
-
"""
|
|
386
|
-
import io
|
|
387
|
-
import wave
|
|
388
|
-
import pyaudio
|
|
389
|
-
import numpy as np
|
|
390
|
-
from pydub import AudioSegment
|
|
391
|
-
|
|
392
|
-
num_device = int(self.window.core.config.get('audio.output.device', 0))
|
|
393
|
-
signals.playback.emit(event_name)
|
|
394
|
-
audio = AudioSegment.from_file(audio_file)
|
|
395
|
-
audio = audio.set_frame_rate(44100) # resample to 44.1 kHz
|
|
396
|
-
wav_io = io.BytesIO()
|
|
397
|
-
audio.export(wav_io, format='wav')
|
|
398
|
-
wav_io.seek(0)
|
|
399
|
-
wf = wave.open(wav_io, 'rb')
|
|
400
|
-
self.pyaudio_instance_output = pyaudio.PyAudio()
|
|
401
|
-
self.stream_output = self.pyaudio_instance_output.open(
|
|
402
|
-
format=self.pyaudio_instance_output.get_format_from_width(wf.getsampwidth()),
|
|
403
|
-
channels=wf.getnchannels(),
|
|
404
|
-
rate=wf.getframerate(),
|
|
405
|
-
output=True,
|
|
406
|
-
output_device_index=num_device
|
|
407
|
-
)
|
|
408
|
-
|
|
409
|
-
sample_width = wf.getsampwidth()
|
|
410
|
-
format = self.pyaudio_instance_output.get_format_from_width(sample_width)
|
|
411
|
-
|
|
412
|
-
if format == pyaudio.paInt8:
|
|
413
|
-
dtype = np.int8
|
|
414
|
-
max_value = 2 ** 7 - 1 # 127
|
|
415
|
-
offset = 0
|
|
416
|
-
elif format == pyaudio.paInt16:
|
|
417
|
-
dtype = np.int16
|
|
418
|
-
max_value = 2 ** 15 - 1 # 32767
|
|
419
|
-
offset = 0
|
|
420
|
-
elif format == pyaudio.paInt32:
|
|
421
|
-
dtype = np.int32
|
|
422
|
-
max_value = 2 ** 31 - 1 # 2147483647
|
|
423
|
-
offset = 0
|
|
424
|
-
elif format == pyaudio.paUInt8:
|
|
425
|
-
dtype = np.uint8
|
|
426
|
-
max_value = 2 ** 8 - 1 # 255
|
|
427
|
-
offset = 128 # center unsigned data
|
|
428
|
-
else:
|
|
429
|
-
raise ValueError(f"Unsupported format: {format}")
|
|
430
|
-
|
|
431
|
-
chunk_size = 512
|
|
432
|
-
data = wf.readframes(chunk_size)
|
|
433
|
-
|
|
434
|
-
while data != b'' and not stopped():
|
|
435
|
-
self.stream_output.write(data)
|
|
436
|
-
|
|
437
|
-
audio_data = np.frombuffer(data, dtype=dtype)
|
|
438
|
-
if len(audio_data) > 0:
|
|
439
|
-
audio_data = audio_data.astype(np.float32)
|
|
440
|
-
if dtype == np.uint8:
|
|
441
|
-
audio_data -= offset
|
|
442
|
-
|
|
443
|
-
# compute RMS
|
|
444
|
-
rms = np.sqrt(np.mean(audio_data ** 2))
|
|
445
|
-
|
|
446
|
-
if rms > 0:
|
|
447
|
-
# RMS to decibels
|
|
448
|
-
db = 20 * np.log10(rms / max_value)
|
|
449
|
-
|
|
450
|
-
# define minimum and maximum dB levels
|
|
451
|
-
min_db = -60 # adjust as needed
|
|
452
|
-
max_db = 0
|
|
453
|
-
|
|
454
|
-
# clamp the db value to the range [min_db, max_db]
|
|
455
|
-
if db < min_db:
|
|
456
|
-
db = min_db
|
|
457
|
-
elif db > max_db:
|
|
458
|
-
db = max_db
|
|
459
|
-
|
|
460
|
-
# map decibel value to volume percentage
|
|
461
|
-
volume_percentage = ((db - min_db) / (max_db - min_db)) * 100
|
|
462
|
-
else:
|
|
463
|
-
volume_percentage = 0
|
|
464
|
-
|
|
465
|
-
# emit volume signal
|
|
466
|
-
signals.volume_changed.emit(volume_percentage)
|
|
467
|
-
else:
|
|
468
|
-
# if empty audio_data
|
|
469
|
-
signals.volume_changed.emit(0)
|
|
470
|
-
|
|
471
|
-
data = wf.readframes(chunk_size)
|
|
472
|
-
|
|
473
|
-
# close the stream
|
|
474
|
-
if self.stream_output is not None:
|
|
475
|
-
if self.stream_output.is_active():
|
|
476
|
-
self.stream_output.stop_stream()
|
|
477
|
-
self.stream_output.close()
|
|
478
|
-
if self.pyaudio_instance_output is not None:
|
|
479
|
-
self.pyaudio_instance_output.terminate()
|
|
480
|
-
|
|
481
|
-
wf.close()
|
|
482
|
-
signals.volume_changed.emit(0)
|
|
483
|
-
|
|
484
|
-
def stop_playback(self, signals=None):
|
|
485
|
-
"""
|
|
486
|
-
Stop audio playback if it is currently playing.
|
|
487
|
-
|
|
488
|
-
:param signals: signals object to emit stop event
|
|
489
|
-
"""
|
|
490
|
-
pass
|
|
491
|
-
|
|
492
|
-
def get_input_devices(self) -> List[Tuple[int, str]]:
|
|
493
|
-
"""
|
|
494
|
-
Get input devices
|
|
495
|
-
|
|
496
|
-
:return devices list: [(id, name)]
|
|
497
|
-
"""
|
|
498
|
-
from bs4 import UnicodeDammit
|
|
499
|
-
self.init()
|
|
500
|
-
devices_list = []
|
|
501
|
-
for item in self.devices:
|
|
502
|
-
index = item['index']
|
|
503
|
-
device_name = item['name']
|
|
504
|
-
dammit = UnicodeDammit(device_name)
|
|
505
|
-
devices_list.append((index, dammit.unicode_markup))
|
|
506
|
-
return devices_list
|
|
507
|
-
|
|
508
|
-
def get_output_devices(self) -> List[Tuple[int, str]]:
|
|
509
|
-
"""
|
|
510
|
-
Get output devices using pyaudio
|
|
511
|
-
|
|
512
|
-
:return: List of tuples with (device_id, device_name)
|
|
513
|
-
"""
|
|
514
|
-
import pyaudio
|
|
515
|
-
p = pyaudio.PyAudio()
|
|
516
|
-
devices_list = []
|
|
517
|
-
for i in range(p.get_device_count()):
|
|
518
|
-
device_info = p.get_device_info_by_index(i)
|
|
519
|
-
if device_info.get('maxOutputChannels', 0) > 0:
|
|
520
|
-
devices_list.append((i, device_info.get('name', 'Unknown')))
|
|
521
|
-
p.terminate()
|
|
522
|
-
return devices_list
|
|
523
|
-
|
|
524
|
-
def get_default_input_device(self) -> tuple:
|
|
525
|
-
"""
|
|
526
|
-
Retrieve the default input device using PyAudio.
|
|
527
|
-
"""
|
|
528
|
-
import pyaudio
|
|
529
|
-
p = pyaudio.PyAudio()
|
|
530
|
-
try:
|
|
531
|
-
default_info = p.get_default_input_device_info()
|
|
532
|
-
device_id = default_info.get('index')
|
|
533
|
-
device_name = default_info.get('name', 'Unknown')
|
|
534
|
-
except IOError as e:
|
|
535
|
-
print("Error getting default output device:", e)
|
|
536
|
-
device_id, device_name = None, None
|
|
537
|
-
p.terminate()
|
|
538
|
-
return device_id, device_name
|
|
539
|
-
|
|
540
|
-
def get_default_output_device(self) -> tuple:
|
|
541
|
-
"""
|
|
542
|
-
Retrieve the default output device using PyAudio.
|
|
543
|
-
"""
|
|
544
|
-
import pyaudio
|
|
545
|
-
p = pyaudio.PyAudio()
|
|
546
|
-
try:
|
|
547
|
-
default_info = p.get_default_output_device_info()
|
|
548
|
-
device_id = default_info.get('index')
|
|
549
|
-
device_name = default_info.get('name', 'Unknown')
|
|
550
|
-
except IOError as e:
|
|
551
|
-
print("Error getting default output device:", e)
|
|
552
|
-
device_id, device_name = None, None
|
|
553
|
-
p.terminate()
|
|
554
|
-
return device_id, device_name
|
|
File without changes
|
|
File without changes
|
|
File without changes
|