pygpt-net 2.4.49__py3-none-any.whl → 2.4.50__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.
CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 2.4.50 (2025-01-16)
4
+
5
+ - Refactored audio input core.
6
+ - Added audio input volume progress bar.
7
+
3
8
  ## 2.4.49 (2025-01-16)
4
9
 
5
10
  - Fix: stream render in Assistants mode.
README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![pygpt](https://snapcraft.io/pygpt/badge.svg)](https://snapcraft.io/pygpt)
4
4
 
5
- Release: **2.4.49** | build: **2025.01.16** | Python: **>=3.10, <3.13**
5
+ Release: **2.4.50** | build: **2025.01.16** | Python: **>=3.10, <3.13**
6
6
 
7
7
  > Official website: https://pygpt.net | Documentation: https://pygpt.readthedocs.io
8
8
  >
@@ -118,6 +118,7 @@ sudo snap connect pygpt:camera
118
118
 
119
119
  ```commandline
120
120
  sudo snap connect pygpt:audio-record :audio-record
121
+ sudo snap connect pygpt:alsa
121
122
  ```
122
123
 
123
124
  **Connecting IPython in Docker in Snap version**:
@@ -3952,6 +3953,11 @@ may consume additional tokens that are not displayed in the main window.
3952
3953
 
3953
3954
  ## Recent changes:
3954
3955
 
3956
+ **2.4.50 (2025-01-16)**
3957
+
3958
+ - Refactored audio input core.
3959
+ - Added audio input volume progress bar.
3960
+
3955
3961
  **2.4.49 (2025-01-16)**
3956
3962
 
3957
3963
  - Fix: stream render in Assistants mode.
pygpt_net/CHANGELOG.txt CHANGED
@@ -1,3 +1,8 @@
1
+ 2.4.50 (2025-01-16)
2
+
3
+ - Refactored audio input core.
4
+ - Added audio input volume progress bar.
5
+
1
6
  2.4.49 (2025-01-16)
2
7
 
3
8
  - Fix: stream render in Assistants mode.
pygpt_net/__init__.py CHANGED
@@ -6,14 +6,14 @@
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.16 01:00:00 #
9
+ # Updated Date: 2025.01.16 17: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.4.49"
16
+ __version__ = "2.4.50"
17
17
  __build__ = "2025.01.16"
18
18
  __maintainer__ = "Marcin Szczygliński"
19
19
  __github__ = "https://github.com/szczyglis-dev/py-gpt"
@@ -6,13 +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: 2024.12.14 08:00:00 #
9
+ # Updated Date: 2025.01.16 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Optional, List, Dict, Any
13
13
 
14
- import pyaudio
15
- import wave
16
14
  import os
17
15
 
18
16
  from PySide6.QtCore import QTimer, Slot
@@ -38,9 +36,6 @@ class Voice:
38
36
  """
39
37
  self.window = window
40
38
  self.is_recording = False
41
- self.frames = []
42
- self.p = None
43
- self.stream = None
44
39
  self.timer = None
45
40
  self.input_file = "voice_control.wav"
46
41
  self.thread_started = False
@@ -218,15 +213,6 @@ class Voice:
218
213
 
219
214
  def start_recording(self):
220
215
  """Start recording"""
221
- self.frames = [] # clear audio frames
222
-
223
- def callback(in_data, frame_count, time_info, status):
224
- self.frames.append(in_data)
225
- if self.is_recording:
226
- return (in_data, pyaudio.paContinue)
227
- else:
228
- return (in_data, pyaudio.paComplete)
229
-
230
216
  try:
231
217
  self.is_recording = True
232
218
  self.switch_btn_stop()
@@ -235,23 +221,26 @@ class Voice:
235
221
  if self.window.controller.audio.is_playing():
236
222
  self.window.controller.audio.stop_output()
237
223
 
224
+ # set audio volume bar
225
+ self.window.core.audio.capture.set_bar(
226
+ self.window.ui.nodes['voice.control.btn'].bar
227
+ )
228
+
238
229
  # start timeout timer to prevent infinite recording
239
230
  if self.timer is None:
240
231
  self.timer = QTimer()
241
232
  self.timer.timeout.connect(self.stop_timeout)
242
233
  self.timer.start(self.TIMEOUT_SECONDS * 1000)
243
234
 
244
- self.p = pyaudio.PyAudio()
245
- self.stream = self.p.open(format=pyaudio.paInt16,
246
- channels=1,
247
- rate=44100,
248
- input=True,
249
- frames_per_buffer=1024,
250
- stream_callback=callback)
235
+ if not self.window.core.audio.capture.check_audio_input():
236
+ raise Exception("Audio input not working.")
237
+ # IMPORTANT!!!!
238
+ # Stop here if audio input not working!
239
+ # This prevents the app from freezing when audio input is not working!
251
240
 
241
+ self.window.core.audio.capture.start() # start recording if audio is OK
252
242
  self.window.update_status(trans('audio.speak.now'))
253
243
  self.window.dispatch(AppEvent(AppEvent.VOICE_CONTROL_STARTED)) # app event
254
- self.stream.start_stream()
255
244
  except Exception as e:
256
245
  self.is_recording = False
257
246
  self.window.core.debug.log(e)
@@ -270,35 +259,29 @@ class Voice:
270
259
 
271
260
  :param timeout: True if stopped due to timeout
272
261
  """
262
+ self.window.core.audio.capture.reset_audio_level()
273
263
  self.is_recording = False
274
264
  if self.timer:
275
265
  self.timer.stop()
276
266
  self.timer = None
277
267
  self.switch_btn_start() # switch button to start
278
268
  path = os.path.join(self.window.core.config.path, self.input_file)
269
+ self.window.core.audio.capture.set_path(path)
279
270
 
280
- if self.stream is not None:
281
- self.stream.stop_stream()
282
- self.stream.close()
283
- self.p.terminate()
284
-
271
+ if self.window.core.audio.capture.has_source():
272
+ self.window.core.audio.capture.stop() # stop recording
285
273
  # abort if timeout
286
274
  if timeout:
287
275
  self.window.dispatch(AppEvent(AppEvent.VOICE_CONTROL_STOPPED)) # app event
288
276
  self.window.update_status("Aborted.".format(self.TIMEOUT_SECONDS))
289
277
  return
290
278
 
291
- if self.frames:
292
- if len(self.frames) < self.MIN_FRAMES:
279
+ if self.window.core.audio.capture.has_frames():
280
+ frames = self.window.core.audio.capture.get_frames()
281
+ if len(frames) < self.MIN_FRAMES:
293
282
  self.window.update_status(trans("status.audio.too_short"))
294
283
  self.window.dispatch(AppEvent(AppEvent.VOICE_CONTROL_STOPPED)) # app event
295
284
  return
296
- wf = wave.open(path, 'wb')
297
- wf.setnchannels(1)
298
- wf.setsampwidth(self.p.get_sample_size(pyaudio.paInt16))
299
- wf.setframerate(44100)
300
- wf.writeframes(b''.join(self.frames))
301
- wf.close()
302
285
  self.window.dispatch(AppEvent(AppEvent.VOICE_CONTROL_SENT)) # app event
303
286
  self.handle_thread(True) # handle transcription in simple mode
304
287
  else:
@@ -6,17 +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: 2024.12.14 18:00:00 #
9
+ # Updated Date: 2025.01.16 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import re
13
13
  from typing import Union, Optional, Tuple, List
14
14
 
15
+ from PySide6.QtMultimedia import QMediaDevices
15
16
  from bs4 import UnicodeDammit
16
17
 
17
18
  from pygpt_net.provider.audio_input.base import BaseProvider as InputBaseProvider
18
19
  from pygpt_net.provider.audio_output.base import BaseProvider as OutputBaseProvider
19
20
 
21
+ from .capture import Capture
20
22
  from .whisper import Whisper
21
23
 
22
24
 
@@ -28,6 +30,7 @@ class Audio:
28
30
  :param window: Window instance
29
31
  """
30
32
  self.window = window
33
+ self.capture = Capture(window)
31
34
  self.whisper = Whisper(window)
32
35
  self.providers = {
33
36
  "input": {},
@@ -41,21 +44,12 @@ class Audio:
41
44
 
42
45
  :return devices list: [(id, name)]
43
46
  """
44
- import pyaudio
45
- devices = []
46
- try:
47
- p = pyaudio.PyAudio()
48
- num_devices = p.get_device_count()
49
- for i in range(num_devices):
50
- info = p.get_device_info_by_index(i)
51
- if info["maxInputChannels"] > 0:
52
- dammit = UnicodeDammit(info["name"])
53
- devices.append((i, dammit.unicode_markup))
54
- # print(f"Device ID {i}: {info['name']}")
55
- p.terminate()
56
- except Exception as e:
57
- print(f"Audio input devices receive error: {e}")
58
- return devices
47
+ devices = QMediaDevices.audioInputs()
48
+ devices_list = []
49
+ for index, device in enumerate(devices):
50
+ dammit = UnicodeDammit(device.description())
51
+ devices_list.append((index, dammit.unicode_markup))
52
+ return devices_list
59
53
 
60
54
  def is_device_compatible(self, device_index: int) -> bool:
61
55
  """
@@ -69,7 +63,6 @@ class Audio:
69
63
  channels = int(self.window.core.config.get('audio.input.channels', 1))
70
64
  p = pyaudio.PyAudio()
71
65
  info = p.get_device_info_by_index(device_index)
72
- supported = False
73
66
  try:
74
67
  p.is_format_supported(
75
68
  rate=rate,
@@ -0,0 +1,331 @@
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.01.16 17:00:00 #
10
+ # ================================================== #
11
+
12
+ import numpy as np
13
+ import wave
14
+
15
+ from PySide6.QtMultimedia import QAudioFormat, QMediaDevices, QAudioSource
16
+
17
+ class Capture:
18
+ def __init__(self, window=None):
19
+ """
20
+ Audio input capture core
21
+
22
+ :param window: Window instance
23
+ """
24
+ self.window = window
25
+ self.audio_source = None
26
+ self.audio_io_device = None
27
+ self.frames = []
28
+ self.actual_audio_format = None
29
+ self.path = None
30
+ self.disconnected = False
31
+ self.devices = []
32
+ self.selected_device = None
33
+ self.bar = None
34
+ self.check_audio_devices()
35
+
36
+ def set_path(self, path: str):
37
+ """
38
+ Set audio input file path
39
+
40
+ :param path: file path
41
+ """
42
+ self.path = path
43
+
44
+ def set_bar(self, bar):
45
+ """
46
+ Set audio level bar
47
+
48
+ :param bar: audio level bar
49
+ """
50
+ self.bar = bar
51
+
52
+ def check_audio_devices(self):
53
+ """Check audio input devices"""
54
+ self.devices = QMediaDevices.audioInputs()
55
+ if not self.devices:
56
+ # no devices found
57
+ self.selected_device = None
58
+ print("No audio input devices found.")
59
+ else:
60
+ # set the first device as default
61
+ self.selected_device = self.devices[0]
62
+
63
+ def device_changed(self, index: int):
64
+ """
65
+ Change audio input device
66
+
67
+ :param index: device index
68
+ """
69
+ if 0 <= index < len(self.devices):
70
+ self.selected_device = self.devices[index]
71
+ else:
72
+ self.selected_device = None
73
+
74
+ def prepare_device(self):
75
+ """Set the current audio input device"""
76
+ device_id = int(self.window.core.config.get('audio.input.device', 0))
77
+ self.device_changed(device_id)
78
+
79
+ def start(self):
80
+ """
81
+ Start audio input recording
82
+
83
+ :return: True if started
84
+ """
85
+ # Clear previous frames
86
+ self.frames = []
87
+
88
+ # Prepare selected device
89
+ self.prepare_device()
90
+ if not self.selected_device:
91
+ print("No audio input device selected")
92
+ return
93
+ if self.disconnected:
94
+ print("Audio source disconnected, please connect the audio source")
95
+ return False
96
+
97
+ # Prevent multiple recordings
98
+ if self.audio_source is not None:
99
+ return False
100
+
101
+ # Set up audio input and start recording
102
+ self.setup_audio_input()
103
+ return True
104
+
105
+ def stop(self):
106
+ """
107
+ Stop audio input recording
108
+
109
+ :return: True if stopped
110
+ """
111
+ result = False
112
+ if self.audio_source is not None:
113
+ # Disconnect the readyRead signal
114
+ self.audio_io_device.readyRead.disconnect(self.process_audio_input)
115
+ self.audio_source.stop()
116
+ self.audio_source = None
117
+ self.audio_io_device = None
118
+
119
+ # Save frames to file (if any)
120
+ if self.frames:
121
+ self.save_audio_file(self.path)
122
+ result = True
123
+ else:
124
+ print("No audio data recorded")
125
+ return result
126
+
127
+ def has_source(self) -> bool:
128
+ """
129
+ Check if audio source is available
130
+
131
+ :return: True if available
132
+ """
133
+ if self.audio_source is not None:
134
+ return True
135
+ return False
136
+
137
+ def has_frames(self) -> bool:
138
+ """
139
+ Check if audio frames are available
140
+
141
+ :return: True if available
142
+ """
143
+ if self.frames:
144
+ return True
145
+ return False
146
+
147
+ def get_frames(self) -> list:
148
+ """
149
+ Get recorded audio frames
150
+
151
+ :return: list of frames
152
+ """
153
+ return self.frames
154
+
155
+ def setup_audio_input(self):
156
+ """Set up audio input device and start recording"""
157
+ if not self.selected_device:
158
+ print("No audio input device selected")
159
+ return
160
+
161
+ # Define audio format
162
+ audio_format = QAudioFormat()
163
+ audio_format.setSampleRate(int(self.window.core.config.get('audio.input.rate', 44100)))
164
+ audio_format.setChannelCount(int(self.window.core.config.get('audio.input.channels', 1)))
165
+ audio_format.setSampleFormat(QAudioFormat.SampleFormat.Int16)
166
+
167
+ # Select default audio input device
168
+ audio_input_device = self.selected_device
169
+
170
+ # Check if the format is supported
171
+ if not audio_input_device.isFormatSupported(audio_format):
172
+ print("Requested format not supported, using nearest format.")
173
+ audio_format = audio_input_device.preferredFormat()
174
+
175
+ # Store the actual format being used
176
+ self.actual_audio_format = audio_format
177
+
178
+ # Create QAudioSource object with the device and format
179
+ try:
180
+ self.audio_source = QAudioSource(audio_input_device, audio_format)
181
+ except Exception as e:
182
+ self.disconnected = True
183
+ print(f"Failed to create audio source: {e}")
184
+
185
+ # Start audio input and obtain the QIODevice
186
+ try:
187
+ self.audio_io_device = self.audio_source.start()
188
+ if self.audio_io_device is None:
189
+ raise Exception("Unable to access audio input device")
190
+ except Exception as e:
191
+ print(f"Failed to start audio input: {e}")
192
+ self.disconnected = True
193
+ self.audio_source = None
194
+ self.audio_io_device = None
195
+ return
196
+
197
+ # Connect the readyRead signal to process incoming data
198
+ self.audio_io_device.readyRead.connect(self.process_audio_input)
199
+
200
+ def process_audio_input(self):
201
+ """Process incoming audio data"""
202
+ data = self.audio_io_device.readAll()
203
+ if data.isEmpty():
204
+ return
205
+
206
+ # Convert QByteArray to bytes
207
+ data_bytes = data.data()
208
+
209
+ # Append raw data to frames list for saving
210
+ self.frames.append(data_bytes)
211
+
212
+ # Determine the correct dtype and normalization factor
213
+ sample_format = self.actual_audio_format.sampleFormat()
214
+ dtype = self.get_dtype_from_sample_format(sample_format)
215
+ normalization_factor = self.get_normalization_factor(sample_format)
216
+
217
+ # Convert bytes to NumPy array of the appropriate type
218
+ samples = np.frombuffer(data_bytes, dtype=dtype)
219
+ if samples.size == 0:
220
+ return
221
+
222
+ # For unsigned formats, center the data
223
+ if sample_format == QAudioFormat.SampleFormat.UInt8:
224
+ samples = samples.astype(np.int16)
225
+ samples -= 128
226
+
227
+ # Compute RMS of the audio samples as float64 for precision
228
+ rms = np.sqrt(np.mean(samples.astype(np.float64) ** 2))
229
+
230
+ # Normalize RMS value based on the sample format
231
+ level = rms / normalization_factor
232
+
233
+ # Ensure level is within 0.0 to 1.0
234
+ level = min(max(level, 0.0), 1.0)
235
+
236
+ # Scale to 0-100
237
+ level_percent = level * 100
238
+
239
+ # Update the level bar widget
240
+ self.update_audio_level(level_percent)
241
+
242
+ def update_audio_level(self, level: int):
243
+ """
244
+ Update the audio level bar
245
+
246
+ :param level: audio level
247
+ """
248
+ if self.bar is not None:
249
+ self.bar.setLevel(level)
250
+
251
+ def reset_audio_level(self):
252
+ """Reset the audio level bar"""
253
+ if self.bar is not None:
254
+ self.bar.setLevel(0)
255
+
256
+ def save_audio_file(self, filename: str):
257
+ """
258
+ Save the recorded audio frames to a WAV file
259
+
260
+ :param filename: output file name
261
+ """
262
+ # Define the parameters for the WAV file
263
+ channels = self.actual_audio_format.channelCount()
264
+ sample_size = self.actual_audio_format.bytesPerSample()
265
+ frame_rate = self.actual_audio_format.sampleRate()
266
+
267
+ # Open the WAV file
268
+ wf = wave.open(filename, 'wb')
269
+ wf.setnchannels(channels)
270
+ wf.setsampwidth(sample_size)
271
+ wf.setframerate(frame_rate)
272
+
273
+ # Write frames to the file
274
+ wf.writeframes(b''.join(self.frames))
275
+ wf.close()
276
+
277
+ def get_dtype_from_sample_format(self, sample_format):
278
+ """
279
+ Get the NumPy dtype corresponding to the QAudioFormat sample format
280
+
281
+ :param sample_format: QAudioFormat.SampleFormat
282
+ """
283
+ if sample_format == QAudioFormat.SampleFormat.UInt8:
284
+ return np.uint8
285
+ elif sample_format == QAudioFormat.SampleFormat.Int16:
286
+ return np.int16
287
+ elif sample_format == QAudioFormat.SampleFormat.Int32:
288
+ return np.int32
289
+ elif sample_format == QAudioFormat.SampleFormat.Float:
290
+ return np.float32
291
+ else:
292
+ raise ValueError("Unsupported sample format")
293
+
294
+ def get_normalization_factor(self, sample_format):
295
+ """
296
+ Get the normalization factor for the QAudioFormat sample format
297
+
298
+ :param sample_format: QAudioFormat.SampleFormat
299
+ """
300
+ if sample_format == QAudioFormat.SampleFormat.UInt8:
301
+ return 255.0
302
+ elif sample_format == QAudioFormat.SampleFormat.Int16:
303
+ return 32768.0
304
+ elif sample_format == QAudioFormat.SampleFormat.Int32:
305
+ return float(2 ** 31)
306
+ elif sample_format == QAudioFormat.SampleFormat.Float:
307
+ return 1.0
308
+ else:
309
+ raise ValueError("Unsupported sample format")
310
+
311
+ def check_audio_input(self) -> bool:
312
+ """
313
+ Check if default audio input device is working
314
+
315
+ :return: True if working
316
+ """
317
+ import pyaudio
318
+ try:
319
+ rate = 44100
320
+ channels = 1
321
+ p = pyaudio.PyAudio()
322
+ stream = p.open(format=pyaudio.paInt16,
323
+ channels=channels,
324
+ rate=rate,
325
+ input=True)
326
+ stream.stop_stream()
327
+ stream.close()
328
+ p.terminate()
329
+ return True
330
+ except Exception as e:
331
+ return False
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "__meta__": {
3
- "version": "2.4.49",
4
- "app.version": "2.4.49",
3
+ "version": "2.4.50",
4
+ "app.version": "2.4.50",
5
5
  "updated_at": "2025-01-16T00:00:00"
6
6
  },
7
7
  "access.audio.event.speech": false,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "__meta__": {
3
- "version": "2.4.49",
4
- "app.version": "2.4.49",
3
+ "version": "2.4.50",
4
+ "app.version": "2.4.50",
5
5
  "updated_at": "2025-01-16T00:00:00"
6
6
  },
7
7
  "items": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "__meta__": {
3
- "version": "2.4.49",
4
- "app.version": "2.4.49",
3
+ "version": "2.4.50",
4
+ "app.version": "2.4.50",
5
5
  "updated_at": "2025-01-16T00:00:00"
6
6
  },
7
7
  "items": {
@@ -6,11 +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: 2024.12.08 00:00:00 #
9
+ # Updated Date: 2025.01.16 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
- import pyaudio
13
- import wave
14
12
  import os
15
13
 
16
14
  from PySide6.QtCore import QTimer
@@ -32,9 +30,6 @@ class Simple:
32
30
  """
33
31
  self.plugin = plugin
34
32
  self.is_recording = False
35
- self.frames = []
36
- self.p = None
37
- self.stream = None
38
33
  self.timer = None
39
34
 
40
35
  def toggle_recording(self):
@@ -61,54 +56,33 @@ class Simple:
61
56
 
62
57
  def start_recording(self):
63
58
  """Start recording"""
64
- self.frames = [] # clear audio frames
65
-
66
- def callback(in_data, frame_count, time_info, status):
67
- self.frames.append(in_data)
68
- if self.is_recording:
69
- return (in_data, pyaudio.paContinue)
70
- else:
71
- return (in_data, pyaudio.paComplete)
72
-
73
59
  try:
74
- self.is_recording = True
75
- self.switch_btn_stop()
76
-
77
60
  # stop audio output if playing
78
61
  if self.plugin.window.controller.audio.is_playing():
79
62
  self.plugin.window.controller.audio.stop_output()
80
63
 
64
+ # set audio volume bar
65
+ self.plugin.window.core.audio.capture.set_bar(
66
+ self.plugin.window.ui.plugin_addon['audio.input.btn'].bar
67
+ )
68
+
81
69
  # start timeout timer to prevent infinite recording
82
70
  if self.timer is None:
83
71
  self.timer = QTimer()
84
72
  self.timer.timeout.connect(self.stop_timeout)
85
73
  self.timer.start(self.TIMEOUT_SECONDS * 1000)
86
74
 
87
- # select audio input device
88
- device_id = int(self.plugin.window.core.config.get('audio.input.device', 0))
89
- rate = int(self.plugin.window.core.config.get('audio.input.rate', 44100))
90
- channels = int(self.plugin.window.core.config.get('audio.input.channels', 1))
91
- if not self.plugin.window.core.audio.is_device_compatible(device_id):
92
- err = self.plugin.window.core.audio.get_last_error()
93
- message = "Selected audio input device is not compatible. Please select another one. ERROR: " + str(err)
94
- self.is_recording = False
95
- self.plugin.window.core.debug.log(message)
96
- self.plugin.window.ui.dialogs.alert(message)
97
- self.switch_btn_start() # switch button to start
98
- return
99
-
100
- self.p = pyaudio.PyAudio()
101
- self.stream = self.p.open(format=pyaudio.paInt16,
102
- channels=channels,
103
- rate=rate,
104
- input=True,
105
- input_device_index=device_id,
106
- frames_per_buffer=1024,
107
- stream_callback=callback)
75
+ if not self.plugin.window.core.audio.capture.check_audio_input():
76
+ raise Exception("Audio input not working.")
77
+ # IMPORTANT!!!!
78
+ # Stop here if audio input not working!
79
+ # This prevents the app from freezing when audio input is not working!
108
80
 
81
+ self.is_recording = True
82
+ self.switch_btn_stop()
83
+ self.plugin.window.core.audio.capture.start() # start recording if audio is OK
109
84
  self.plugin.window.update_status(trans('audio.speak.now'))
110
85
  self.plugin.window.dispatch(AppEvent(AppEvent.INPUT_VOICE_LISTEN_STARTED)) # app event
111
- self.stream.start_stream()
112
86
  except Exception as e:
113
87
  self.is_recording = False
114
88
  self.plugin.window.core.debug.log(e)
@@ -127,6 +101,7 @@ class Simple:
127
101
 
128
102
  :param timeout: True if stopped due to timeout
129
103
  """
104
+ self.plugin.window.core.audio.capture.reset_audio_level()
130
105
  self.is_recording = False
131
106
  self.plugin.window.dispatch(AppEvent(AppEvent.INPUT_VOICE_LISTEN_STOPPED)) # app event
132
107
  if self.timer:
@@ -134,30 +109,22 @@ class Simple:
134
109
  self.timer = None
135
110
  self.switch_btn_start() # switch button to start
136
111
  path = os.path.join(self.plugin.window.core.config.path, self.plugin.input_file)
112
+ self.plugin.window.core.audio.capture.set_path(path)
137
113
 
138
- if self.stream is not None:
139
- self.stream.stop_stream()
140
- self.stream.close()
141
- self.p.terminate()
142
-
114
+ if self.plugin.window.core.audio.capture.has_source():
115
+ self.plugin.window.core.audio.capture.stop() # stop recording
143
116
  # abort if timeout
144
117
  if timeout:
145
118
  self.plugin.window.update_status("Aborted.".format(self.TIMEOUT_SECONDS))
146
119
  return
147
120
 
148
- if self.frames:
149
- if len(self.frames) < self.MIN_FRAMES:
121
+ if self.plugin.window.core.audio.capture.has_frames():
122
+ frames = self.plugin.window.core.audio.capture.get_frames()
123
+ if len(frames) < self.MIN_FRAMES:
150
124
  self.plugin.window.update_status(trans("status.audio.too_short"))
151
125
  self.plugin.window.dispatch(AppEvent(AppEvent.VOICE_CONTROL_STOPPED)) # app event
152
126
  return
153
- wf = wave.open(path, 'wb')
154
- channels = int(self.plugin.window.core.config.get('audio.input.channels', 1))
155
- rate = int(self.plugin.window.core.config.get('audio.input.rate', 44100))
156
- wf.setnchannels(channels)
157
- wf.setsampwidth(self.p.get_sample_size(pyaudio.paInt16))
158
- wf.setframerate(rate)
159
- wf.writeframes(b''.join(self.frames))
160
- wf.close()
127
+
161
128
  self.plugin.handle_thread(True) # handle transcription in simple mode
162
129
  else:
163
130
  self.plugin.window.update_status("")
@@ -6,11 +6,12 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2024.11.20 21:00:00 #
9
+ # Updated Date: 2025.01.16 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import Qt
13
- from PySide6.QtWidgets import QLabel, QHBoxLayout, QWidget, QPushButton
13
+ from PySide6.QtGui import QPainter
14
+ from PySide6.QtWidgets import QLabel, QHBoxLayout, QWidget, QPushButton, QVBoxLayout
14
15
 
15
16
  from pygpt_net.core.events import Event, AppEvent
16
17
  from pygpt_net.utils import trans
@@ -31,14 +32,18 @@ class VoiceControlButton(QWidget):
31
32
  self.btn_toggle.setCursor(Qt.PointingHandCursor)
32
33
  self.btn_toggle.setMinimumWidth(200)
33
34
 
35
+ self.bar = LevelBar(self)
36
+ self.bar.setLevel(0)
37
+
34
38
  # status
35
39
  self.status = QLabel("")
36
40
  self.status.setStyleSheet("color: #999; font-size: 10px; font-weight: 400; margin: 0; padding: 0; border: 0;")
37
41
  self.status.setMaximumHeight(15)
38
42
 
39
- self.layout = QHBoxLayout(self)
43
+ self.layout = QVBoxLayout(self)
40
44
  self.layout.addWidget(self.btn_toggle)
41
- self.layout.addWidget(self.status)
45
+ # self.layout.addWidget(self.status)
46
+ self.layout.addWidget(self.bar)
42
47
 
43
48
  # self.layout.addWidget(self.stop)
44
49
  self.layout.setAlignment(Qt.AlignCenter)
@@ -82,14 +87,18 @@ class AudioInputButton(QWidget):
82
87
  self.btn_toggle.setCursor(Qt.PointingHandCursor)
83
88
  self.btn_toggle.setMinimumWidth(200)
84
89
 
90
+ self.bar = LevelBar(self)
91
+ self.bar.setLevel(0)
92
+
85
93
  # status
86
- self.status = QLabel("")
87
- self.status.setStyleSheet("color: #999; font-size: 10px; font-weight: 400; margin: 0; padding: 0; border: 0;")
88
- self.status.setMaximumHeight(15)
94
+ #self.status = QLabel("xxx")
95
+ #self.status.setStyleSheet("color: #999; font-size: 10px; font-weight: 400; margin: 0; padding: 0; border: 0;")
96
+ #self.status.setMaximumHeight(15)
89
97
 
90
- self.layout = QHBoxLayout(self)
98
+ self.layout = QVBoxLayout(self)
91
99
  self.layout.addWidget(self.btn_toggle)
92
- self.layout.addWidget(self.status)
100
+ #self.layout.addWidget(self.status)
101
+ self.layout.addWidget(self.bar)
93
102
 
94
103
  # self.layout.addWidget(self.stop)
95
104
  self.layout.setAlignment(Qt.AlignCenter)
@@ -116,3 +125,46 @@ class AudioInputButton(QWidget):
116
125
  """Toggle recording"""
117
126
  event = Event(Event.AUDIO_INPUT_RECORD_TOGGLE)
118
127
  self.window.dispatch(event)
128
+
129
+
130
+ class LevelBar(QWidget):
131
+ def __init__(self, parent=None):
132
+ super().__init__(parent)
133
+ self._level = 0.0 # level from 0.0 to 100.0
134
+ self.setFixedSize(200, 5) # bar size
135
+
136
+ def setLevel(self, level):
137
+ """
138
+ Set volume level
139
+
140
+ :param level: level
141
+ """
142
+ self._level = level
143
+ self.update()
144
+
145
+ def paintEvent(self, event):
146
+ """
147
+ Paint event
148
+
149
+ :param event: event
150
+ """
151
+ painter = QPainter(self)
152
+ painter.fillRect(self.rect(), Qt.transparent)
153
+ level_width = (self._level / 100.0) * self.width()
154
+ painter.setBrush(Qt.green)
155
+ painter.setPen(Qt.NoPen)
156
+ painter.drawRect(0, 0, level_width, self.height())
157
+
158
+ """
159
+ # --- bar from center ---
160
+ def paintEvent(self, event):
161
+ painter = QPainter(self)
162
+ painter.fillRect(self.rect(), Qt.transparent)
163
+ level_width = (self._level / 100.0) * self.width()
164
+ half_level_width = level_width / 2
165
+ center_x = self.width() / 2
166
+ rect_x = center_x - half_level_width
167
+ painter.setBrush(Qt.green)
168
+ painter.setPen(Qt.NoPen)
169
+ painter.drawRect(rect_x, 0, level_width, self.height())
170
+ """
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2024.02.26 22:00:00 #
9
+ # Updated Date: 2025.01.16 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtWidgets import QDialog, QLabel, QVBoxLayout, QPushButton
@@ -55,7 +55,7 @@ class SnapDialogAudioInput(QDialog):
55
55
  self.window = window
56
56
  self.setParent(window)
57
57
  self.setWindowTitle("Snap is detected")
58
- self.cmd = CmdLabel(self.window, "sudo snap connect pygpt:audio-record :audio-record")
58
+ self.cmd = CmdLabel(self.window, "sudo snap connect pygpt:alsa && sudo snap connect pygpt:audio-record :audio-record")
59
59
 
60
60
  self.btn = QPushButton("OK")
61
61
  self.btn.clicked.connect(self.accept)
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2024.02.17 20:00:00 #
9
+ # Updated Date: 2025.01.16 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -177,7 +177,8 @@ class UpdateDialog(BaseDialog):
177
177
  if self.window.core.platforms.is_windows():
178
178
  self.download_link = download_windows
179
179
  self.download_file.setText("{} .msi ({})".format(trans("action.download"), version))
180
- self.download_file.setVisible(True)
180
+ self.download_file.setVisible(False) # Windows Store: disabled
181
+ self.info_upgrade.setVisible(False) # Windows Store: disabled
181
182
  elif self.window.core.platforms.is_linux():
182
183
  self.download_link = download_linux
183
184
  self.download_file.setText("{} .tar.gz ({})".format(trans("action.download"), version))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pygpt-net
3
- Version: 2.4.49
3
+ Version: 2.4.50
4
4
  Summary: Desktop AI Assistant powered by models: OpenAI o1, GPT-4o, GPT-4, GPT-4 Vision, GPT-3.5, DALL-E 3, Llama 3, Mistral, Gemini, Claude, Bielik, and other models supported by Langchain, Llama Index, and Ollama. Features include chatbot, text completion, image generation, vision analysis, speech-to-text, internet access, file handling, command execution and more.
5
5
  Home-page: https://pygpt.net
6
6
  License: MIT
@@ -93,7 +93,7 @@ Description-Content-Type: text/markdown
93
93
 
94
94
  [![pygpt](https://snapcraft.io/pygpt/badge.svg)](https://snapcraft.io/pygpt)
95
95
 
96
- Release: **2.4.49** | build: **2025.01.16** | Python: **>=3.10, <3.13**
96
+ Release: **2.4.50** | build: **2025.01.16** | Python: **>=3.10, <3.13**
97
97
 
98
98
  > Official website: https://pygpt.net | Documentation: https://pygpt.readthedocs.io
99
99
  >
@@ -209,6 +209,7 @@ sudo snap connect pygpt:camera
209
209
 
210
210
  ```commandline
211
211
  sudo snap connect pygpt:audio-record :audio-record
212
+ sudo snap connect pygpt:alsa
212
213
  ```
213
214
 
214
215
  **Connecting IPython in Docker in Snap version**:
@@ -4043,6 +4044,11 @@ may consume additional tokens that are not displayed in the main window.
4043
4044
 
4044
4045
  ## Recent changes:
4045
4046
 
4047
+ **2.4.50 (2025-01-16)**
4048
+
4049
+ - Refactored audio input core.
4050
+ - Added audio input volume progress bar.
4051
+
4046
4052
  **2.4.49 (2025-01-16)**
4047
4053
 
4048
4054
  - Fix: stream render in Assistants mode.
@@ -1,16 +1,16 @@
1
- CHANGELOG.md,sha256=gFRfBiL4PYqprFVvaUlx9AqT5AVpmhVjPK-YRLxle40,80939
2
- README.md,sha256=qkwiNaUhyZKE-g0wWxYUyd946edChMkH9Y-mPDOajG4,164846
1
+ CHANGELOG.md,sha256=adPwRkkYgkZHuwQn1IB8ImnyuwtpSXTJoKvdA3zwEzI,81036
2
+ README.md,sha256=WheYr0os5j1AVG5X4ifEyjK5swLluh01k7PCv8-wj5k,164973
3
3
  icon.png,sha256=CzcINJaU23a9hNjsDlDNbyuiEvKZ4Wg6DQVYF6SpuRg,13970
4
- pygpt_net/CHANGELOG.txt,sha256=-X0u1bg5Oq4rebB68yD00Movnr0rEPiC8aItUVHpn68,79456
4
+ pygpt_net/CHANGELOG.txt,sha256=B1Gu0sgfveaUUwyPeqflV-uSmsdJbE39JSrukL92QM0,79550
5
5
  pygpt_net/LICENSE,sha256=dz9sfFgYahvu2NZbx4C1xCsVn9GVer2wXcMkFRBvqzY,1146
6
- pygpt_net/__init__.py,sha256=izcO_wLOAUU_jEhc6pCud7V2w6ywEptBfnZqqSstZnU,1307
6
+ pygpt_net/__init__.py,sha256=u1pfEPJl3_fBwyjhEFbmNcNZv1jJ250_17nt8EK9XjY,1307
7
7
  pygpt_net/app.py,sha256=i02M96uLngAs_XZCS1Mi84vb3Okx8ZZewbTdhCqFolM,16029
8
8
  pygpt_net/config.py,sha256=Qc1FOBtTf3O6A6-6KoqUGtoJ0u8hXQeowvCVbZFwtik,16405
9
9
  pygpt_net/container.py,sha256=BemiVZPpPNIzfB-ZvnZeeBPFu-AcX2c30OqYFylEjJc,4023
10
10
  pygpt_net/controller/__init__.py,sha256=wtlkw4viFVuf2sP0iMSkK0jI1QMa3kcPfvQaqnC-1io,5917
11
11
  pygpt_net/controller/access/__init__.py,sha256=5DzR7zVmFsOICo9I5mEQcIOgsk2WCNi1amUWxExwUiY,2639
12
12
  pygpt_net/controller/access/control.py,sha256=nMGWzg60jNJMVAHIrism0_APzVMpbLAOcXG6mJuOSJ8,17332
13
- pygpt_net/controller/access/voice.py,sha256=Mt8waiGSc0p-aM8O_8OcGYjAxb0okgG0M1aHVbim9ok,15532
13
+ pygpt_net/controller/access/voice.py,sha256=SJ6AdNOfvWWbegjLuNLrJTZ0K9uxcWx1Q47zVCXOsN8,15184
14
14
  pygpt_net/controller/agent/__init__.py,sha256=bArPuyZOJujcf4l1GKMMQ7Wx--xBoFHXz1A3TDjNjAk,1267
15
15
  pygpt_net/controller/agent/common.py,sha256=55CHhV-dsWeNe5QvdvNoyhEYVhQNrHt_Lv-VDTuiYRc,3871
16
16
  pygpt_net/controller/agent/experts.py,sha256=qllzrjVNfDoueBc0nsQLaWlOY4wpUKENp4l9cjWz0BM,5830
@@ -124,7 +124,8 @@ pygpt_net/core/assistants/store.py,sha256=4zz8_10_f6o8gdRekEPo5Ox0tLwuZO8tKyVsz-
124
124
  pygpt_net/core/attachments/__init__.py,sha256=bUqvfPqlpdXiGf3GvS1kTE45A0Q1Eo3kpUKypnRwDpk,12919
125
125
  pygpt_net/core/attachments/context.py,sha256=tQM3z_gLI99Ox47XZtVcUnOdfaPYKQwXhm1EJaNikvE,25057
126
126
  pygpt_net/core/attachments/worker.py,sha256=_aUCyi5-Mbz0IGfgY6QKBZ6MFz8aKRDfKasbBVXg7kU,1341
127
- pygpt_net/core/audio/__init__.py,sha256=78xr4fNQbj-0eWGwq3XuSdbIi7iLYn414C5cn_TWp18,4766
127
+ pygpt_net/core/audio/__init__.py,sha256=uszH6pqMToDzL0WpPeUvVlyJ8RN4gFmQbsL4GFYMIdc,4521
128
+ pygpt_net/core/audio/capture.py,sha256=BBxXM7KxxCxcvN7cW0b5dsSi8BphiLzEfkIt8PfN77Y,10408
128
129
  pygpt_net/core/audio/context.py,sha256=2XpXWhDC09iUvc0FRMq9BF2_rnQ60ZG4Js6LbO5MohY,1115
129
130
  pygpt_net/core/audio/whisper.py,sha256=WZ_fNQ06s1NBxyoYB-lTFqDO6ARcnq9MZFekRaTNxTo,993
130
131
  pygpt_net/core/bridge/__init__.py,sha256=ezS02_2wUrnV6eTF33wfob8rVWM5snfY92-PF_i15uQ,9568
@@ -246,9 +247,9 @@ pygpt_net/css_rc.py,sha256=i13kX7irhbYCWZ5yJbcMmnkFp_UfS4PYnvRFSPF7XXo,11349
246
247
  pygpt_net/data/audio/click_off.mp3,sha256=aNiRDP1pt-Jy7ija4YKCNFBwvGWbzU460F4pZWZDS90,65201
247
248
  pygpt_net/data/audio/click_on.mp3,sha256=qfdsSnthAEHVXzeyN4LlC0OvXuyW8p7stb7VXtlvZ1k,65201
248
249
  pygpt_net/data/audio/ok.mp3,sha256=LTiV32pEBkpUGBkKkcOdOFB7Eyt_QoP2Nv6c5AaXftk,32256
249
- pygpt_net/data/config/config.json,sha256=5HkaHlyhOoefxyM5HL1Oj_IhnC8PCFcaJ7dc6FPLOxc,19735
250
- pygpt_net/data/config/models.json,sha256=lrki8Hq-HafH_Nr-ijmoCtrFCmgll_hKdnlshsg5ag0,61940
251
- pygpt_net/data/config/modes.json,sha256=wxKhY9HJBjkj0lEtkwnod7aaz_iJmWY4eEyCVxY_5RU,1923
250
+ pygpt_net/data/config/config.json,sha256=ezfhWuQCpy1HWm69Pc_7eVdtese2vwheLC3tb62h6Mo,19735
251
+ pygpt_net/data/config/models.json,sha256=lhdi2Y9w3GxpRmpp07embuE4TVixvml9wbcoylMQA_8,61940
252
+ pygpt_net/data/config/modes.json,sha256=KILDDam2UjJ-dCj8_PF9AnTlj4Xq4Ms9HYxOq2Krx-M,1923
252
253
  pygpt_net/data/config/presets/agent_openai.json,sha256=vMTR-soRBiEZrpJJHuFLWyx8a3Ez_BqtqjyXgxCAM_Q,733
253
254
  pygpt_net/data/config/presets/agent_openai_assistant.json,sha256=awJw9lNTGpKML6SJUShVn7lv8AXh0oic7wBeyoN7AYs,798
254
255
  pygpt_net/data/config/presets/agent_planner.json,sha256=a6Rv58Bnm2STNWB0Rw_dGhnsz6Lb3J8_GwsUVZaTIXc,742
@@ -1700,7 +1701,7 @@ pygpt_net/plugin/agent/__init__.py,sha256=GPbnpS9djcfzuPRxM2mu0hpUPRa51KtUR27Al6
1700
1701
  pygpt_net/plugin/agent/config.py,sha256=V4M0boutzxFVWTQxM8UA7HgKUR4v_Y-5dX_XfeLzzL8,9792
1701
1702
  pygpt_net/plugin/audio_input/__init__.py,sha256=vGwDtsPkwCEwiz2ePnzW48Tuhr0VHEc7kQua11VE7tI,15714
1702
1703
  pygpt_net/plugin/audio_input/config.py,sha256=x57IVxBapJp9rwos327T6U0jTFSPeRJ6BorqfYxJ4u0,9197
1703
- pygpt_net/plugin/audio_input/simple.py,sha256=ea89IpCk_f-byqxdjDPCb3vvZH5nirO3ceP7-U54_Sc,6561
1704
+ pygpt_net/plugin/audio_input/simple.py,sha256=N5jL_hYp4ZvUBXqoiyx3Tt3b4bWKc9SbKOTtD00TNTE,5195
1704
1705
  pygpt_net/plugin/audio_input/worker.py,sha256=kG7r08ot1h0Jyw_UQ_sFN1rOKOkOC-ByLbCp-oZZBr4,11828
1705
1706
  pygpt_net/plugin/audio_output/__init__.py,sha256=wR_gBQgJ_arxHPSvjtT7naNwkNrCJc6KFR7ZUAqYcvc,7896
1706
1707
  pygpt_net/plugin/audio_output/config.py,sha256=IA2K-9fQMZSwYGyi30Uh5qAlYwuqwaHo3dtDJ13vQdo,1208
@@ -2073,7 +2074,7 @@ pygpt_net/ui/widget/anims/loader.py,sha256=PzxHraeABUyMIZlg4Rk_tbJnUPmiwxlhdcHaC
2073
2074
  pygpt_net/ui/widget/anims/toggles.py,sha256=_L2533IYyDkbnPCok9XBriIaKM5E9sHSznrwVWpKOOs,5755
2074
2075
  pygpt_net/ui/widget/audio/__init__.py,sha256=8HT4tQFqQogEEpGYTv2RplKBthlsFKcl5egnv4lzzEw,488
2075
2076
  pygpt_net/ui/widget/audio/input.py,sha256=t9VAhP15HkSOvNV2crI3Kg6AgrQDj-wSQiiYTMlvK60,1721
2076
- pygpt_net/ui/widget/audio/input_button.py,sha256=e5nt91PcLZK0fNbzhvHeRYdpBzRNz-iusoPZziajs30,3695
2077
+ pygpt_net/ui/widget/audio/input_button.py,sha256=ykhBIRZ0g2w2f5YxOMkxtOndLDbdW4G8x3UbP1rVKyk,5185
2077
2078
  pygpt_net/ui/widget/audio/output.py,sha256=UxkiCnVT9DNFeByDGTFW_CK0LW8xSvhEK1zygtHvx4k,1586
2078
2079
  pygpt_net/ui/widget/calendar/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2079
2080
  pygpt_net/ui/widget/calendar/select.py,sha256=vSEaQZmyeDhmyOfptWv6me_6IaYpQb5Jgc_mGDIe_4U,9051
@@ -2100,8 +2101,8 @@ pygpt_net/ui/widget/dialog/profile.py,sha256=vMYf-9c6HKAOqtSuw-HpBEUgmCSxEQuIrGl
2100
2101
  pygpt_net/ui/widget/dialog/rename.py,sha256=HcImKH_gUHcyB1k5llwStmrlXvcSpxgR2V6IuEdMuCU,2191
2101
2102
  pygpt_net/ui/widget/dialog/settings.py,sha256=fKzbme2tdxzTSiQMNnCEgyD3lwCzFjLi85ikWulisGw,1614
2102
2103
  pygpt_net/ui/widget/dialog/settings_plugin.py,sha256=Kf1ZK_RY9CAnfeuzPoQ4wgsFb2yQl7X-VKzsYETA55o,1696
2103
- pygpt_net/ui/widget/dialog/snap.py,sha256=ESGmSSpExvf6WJ_EwiHJ9QK-t0NccfWeym5SSJNfi8I,2724
2104
- pygpt_net/ui/widget/dialog/update.py,sha256=0z4motDKCchD8whFHGZJui9omM_dmpviqi3g5yjy6p0,6789
2104
+ pygpt_net/ui/widget/dialog/snap.py,sha256=UKiHOywK1tw0HelSgsUaKXMlageDdzOH_TJot3lUVUg,2756
2105
+ pygpt_net/ui/widget/dialog/update.py,sha256=aAmk-unSaGie3VXMaXmorza-p9Isrm06d0iSYnfcq3U,6900
2105
2106
  pygpt_net/ui/widget/dialog/url.py,sha256=7I17Pp9P2c3G1pODEY5dum_AF0nFnu2BMfbWTgEES-M,8765
2106
2107
  pygpt_net/ui/widget/dialog/workdir.py,sha256=D-C3YIt-wCoI-Eh7z--Z4R6P1UvtpkxeiaVcI-ycFck,1523
2107
2108
  pygpt_net/ui/widget/draw/__init__.py,sha256=oSYKtNEGNL0vDjn3wCgdnBAbxUqNGIEIf-75I2DIn7Q,488
@@ -2174,8 +2175,8 @@ pygpt_net/ui/widget/textarea/web.py,sha256=9FoL02QY6mOxtc4t4fe8X7fVDIdPn9Sb_fwsv
2174
2175
  pygpt_net/ui/widget/vision/__init__.py,sha256=8HT4tQFqQogEEpGYTv2RplKBthlsFKcl5egnv4lzzEw,488
2175
2176
  pygpt_net/ui/widget/vision/camera.py,sha256=T8b5cmK6uhf_WSSxzPt_Qod8JgMnst6q8sQqRvgQiSA,2584
2176
2177
  pygpt_net/utils.py,sha256=Gsh_mITVke3bb8o-Ke57l__xA5a9Wv4t7tlsnSQULj8,6655
2177
- pygpt_net-2.4.49.dist-info/LICENSE,sha256=GLKQTnJOPK4dDIWfkAIM4GwOxKJXi5zcMGt7FjLR1xk,1126
2178
- pygpt_net-2.4.49.dist-info/METADATA,sha256=j-CVH60IOvIiJj9FCjbFCeZBjm6stvp23QrJ7oZR85Q,169725
2179
- pygpt_net-2.4.49.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
2180
- pygpt_net-2.4.49.dist-info/entry_points.txt,sha256=qvpII6UHIt8XfokmQWnCYQrTgty8FeJ9hJvOuUFCN-8,43
2181
- pygpt_net-2.4.49.dist-info/RECORD,,
2178
+ pygpt_net-2.4.50.dist-info/LICENSE,sha256=GLKQTnJOPK4dDIWfkAIM4GwOxKJXi5zcMGt7FjLR1xk,1126
2179
+ pygpt_net-2.4.50.dist-info/METADATA,sha256=kttDYqtQSS0wi5lXkI3Y7APenTEcXuIVcRiuspWa_7Q,169852
2180
+ pygpt_net-2.4.50.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
2181
+ pygpt_net-2.4.50.dist-info/entry_points.txt,sha256=qvpII6UHIt8XfokmQWnCYQrTgty8FeJ9hJvOuUFCN-8,43
2182
+ pygpt_net-2.4.50.dist-info/RECORD,,