pygpt-net 2.4.49__py3-none-any.whl → 2.4.51__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. CHANGELOG.md +10 -0
  2. README.md +12 -62
  3. pygpt_net/CHANGELOG.txt +10 -0
  4. pygpt_net/__init__.py +3 -3
  5. pygpt_net/controller/access/voice.py +19 -36
  6. pygpt_net/controller/audio/__init__.py +15 -1
  7. pygpt_net/controller/lang/custom.py +2 -1
  8. pygpt_net/controller/ui/tabs.py +7 -1
  9. pygpt_net/core/audio/__init__.py +10 -17
  10. pygpt_net/core/audio/capture.py +349 -0
  11. pygpt_net/data/config/config.json +5 -3
  12. pygpt_net/data/config/models.json +3 -3
  13. pygpt_net/data/config/modes.json +3 -3
  14. pygpt_net/data/config/settings.json +13 -0
  15. pygpt_net/data/locale/locale.de.ini +3 -0
  16. pygpt_net/data/locale/locale.en.ini +3 -0
  17. pygpt_net/data/locale/locale.es.ini +3 -0
  18. pygpt_net/data/locale/locale.fr.ini +3 -0
  19. pygpt_net/data/locale/locale.it.ini +3 -0
  20. pygpt_net/data/locale/locale.pl.ini +3 -0
  21. pygpt_net/data/locale/locale.uk.ini +3 -0
  22. pygpt_net/data/locale/locale.zh.ini +3 -0
  23. pygpt_net/plugin/audio_input/simple.py +45 -55
  24. pygpt_net/provider/core/config/patch.py +9 -1
  25. pygpt_net/ui/layout/chat/input.py +0 -12
  26. pygpt_net/ui/menu/__init__.py +4 -3
  27. pygpt_net/ui/widget/audio/input_button.py +84 -24
  28. pygpt_net/ui/widget/dialog/snap.py +2 -2
  29. pygpt_net/ui/widget/dialog/update.py +3 -2
  30. {pygpt_net-2.4.49.dist-info → pygpt_net-2.4.51.dist-info}/METADATA +13 -63
  31. {pygpt_net-2.4.49.dist-info → pygpt_net-2.4.51.dist-info}/RECORD +34 -33
  32. {pygpt_net-2.4.49.dist-info → pygpt_net-2.4.51.dist-info}/LICENSE +0 -0
  33. {pygpt_net-2.4.49.dist-info → pygpt_net-2.4.51.dist-info}/WHEEL +0 -0
  34. {pygpt_net-2.4.49.dist-info → pygpt_net-2.4.51.dist-info}/entry_points.txt +0 -0
@@ -6,16 +6,15 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2024.12.08 00:00:00 #
9
+ # Updated Date: 2025.01.17 02: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
17
15
 
18
16
  from pygpt_net.core.events import AppEvent
17
+ from pygpt_net.core.tabs.tab import Tab
19
18
  from pygpt_net.utils import trans
20
19
 
21
20
 
@@ -32,9 +31,6 @@ class Simple:
32
31
  """
33
32
  self.plugin = plugin
34
33
  self.is_recording = False
35
- self.frames = []
36
- self.p = None
37
- self.stream = None
38
34
  self.timer = None
39
35
 
40
36
  def toggle_recording(self):
@@ -59,56 +55,48 @@ class Simple:
59
55
  """Stop timeout"""
60
56
  self.stop_recording(timeout=True)
61
57
 
62
- def start_recording(self):
63
- """Start recording"""
64
- self.frames = [] # clear audio frames
58
+ def start_recording(self, force: bool = False):
59
+ """
60
+ Start recording
65
61
 
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)
62
+ :param force: True to force recording
63
+ """
64
+ # enable continuous mode if notepad tab is active
65
+ self.plugin.window.core.audio.capture.stop_callback = self.on_stop
66
+ continuous_enabled = self.plugin.window.core.config.get('audio.input.continuous', False)
67
+ if continuous_enabled and self.plugin.window.controller.ui.tabs.get_current_type() == Tab.TAB_NOTEPAD:
68
+ self.plugin.window.core.audio.capture.loop = True # set loop
69
+ else:
70
+ self.plugin.window.core.audio.capture.loop = False
72
71
 
73
72
  try:
74
- self.is_recording = True
75
- self.switch_btn_stop()
76
-
77
73
  # stop audio output if playing
78
74
  if self.plugin.window.controller.audio.is_playing():
79
75
  self.plugin.window.controller.audio.stop_output()
80
76
 
77
+ # set audio volume bar
78
+ self.plugin.window.core.audio.capture.set_bar(
79
+ self.plugin.window.ui.plugin_addon['audio.input.btn'].bar
80
+ )
81
+
81
82
  # start timeout timer to prevent infinite recording
82
83
  if self.timer is None:
83
84
  self.timer = QTimer()
84
85
  self.timer.timeout.connect(self.stop_timeout)
85
86
  self.timer.start(self.TIMEOUT_SECONDS * 1000)
86
87
 
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)
88
+ if not force:
89
+ if not self.plugin.window.core.audio.capture.check_audio_input():
90
+ raise Exception("Audio input not working.")
91
+ # IMPORTANT!!!!
92
+ # Stop here if audio input not working!
93
+ # This prevents the app from freezing when audio input is not working!
108
94
 
95
+ self.is_recording = True
96
+ self.switch_btn_stop()
97
+ self.plugin.window.core.audio.capture.start() # start recording if audio is OK
109
98
  self.plugin.window.update_status(trans('audio.speak.now'))
110
99
  self.plugin.window.dispatch(AppEvent(AppEvent.INPUT_VOICE_LISTEN_STARTED)) # app event
111
- self.stream.start_stream()
112
100
  except Exception as e:
113
101
  self.is_recording = False
114
102
  self.plugin.window.core.debug.log(e)
@@ -127,6 +115,7 @@ class Simple:
127
115
 
128
116
  :param timeout: True if stopped due to timeout
129
117
  """
118
+ self.plugin.window.core.audio.capture.reset_audio_level()
130
119
  self.is_recording = False
131
120
  self.plugin.window.dispatch(AppEvent(AppEvent.INPUT_VOICE_LISTEN_STOPPED)) # app event
132
121
  if self.timer:
@@ -134,30 +123,31 @@ class Simple:
134
123
  self.timer = None
135
124
  self.switch_btn_start() # switch button to start
136
125
  path = os.path.join(self.plugin.window.core.config.path, self.plugin.input_file)
126
+ self.plugin.window.core.audio.capture.set_path(path)
137
127
 
138
- if self.stream is not None:
139
- self.stream.stop_stream()
140
- self.stream.close()
141
- self.p.terminate()
142
-
128
+ if self.plugin.window.core.audio.capture.has_source():
129
+ self.plugin.window.core.audio.capture.stop() # stop recording
143
130
  # abort if timeout
144
131
  if timeout:
145
132
  self.plugin.window.update_status("Aborted.".format(self.TIMEOUT_SECONDS))
146
133
  return
147
134
 
148
- if self.frames:
149
- if len(self.frames) < self.MIN_FRAMES:
135
+ if self.plugin.window.core.audio.capture.has_frames():
136
+ frames = self.plugin.window.core.audio.capture.get_frames()
137
+ if len(frames) < self.MIN_FRAMES:
150
138
  self.plugin.window.update_status(trans("status.audio.too_short"))
151
139
  self.plugin.window.dispatch(AppEvent(AppEvent.VOICE_CONTROL_STOPPED)) # app event
152
140
  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()
141
+
161
142
  self.plugin.handle_thread(True) # handle transcription in simple mode
162
143
  else:
163
144
  self.plugin.window.update_status("")
145
+
146
+
147
+ def on_stop(self):
148
+ """Handle auto-transcribe"""
149
+ path = os.path.join(self.plugin.window.core.config.path, self.plugin.input_file)
150
+ self.plugin.window.core.audio.capture.set_path(path)
151
+ self.plugin.window.core.audio.capture.stop()
152
+ self.plugin.window.core.audio.capture.start()
153
+ self.plugin.handle_thread(True)
@@ -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.12.16 20:00:00 #
9
+ # Updated Date: 2025.01.17 02:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
@@ -1803,6 +1803,14 @@ class Patch:
1803
1803
  data["api_key_hugging_face"] = ""
1804
1804
  updated = True
1805
1805
 
1806
+ # < 2.4.51
1807
+ if old < parse_version("2.4.51"):
1808
+ print("Migrating config from < 2.4.51...")
1809
+ if 'audio.input.stop_interval' not in data:
1810
+ data["audio.input.stop_interval"] = 10
1811
+ if 'audio.input.continuous' not in data:
1812
+ data["audio.input.continuous"] = False
1813
+
1806
1814
  # update file
1807
1815
  migrated = False
1808
1816
  if updated:
@@ -160,24 +160,12 @@ class Input:
160
160
 
161
161
  grid = QGridLayout()
162
162
 
163
- #left_layout = QHBoxLayout()
164
- #left_layout.addWidget(self.window.ui.nodes['input.label'])
165
- #left_layout.addWidget(self.window.ui.nodes['inline.vision'])
166
- #left_layout.addStretch(1)
167
-
168
163
  center_layout = QHBoxLayout()
169
164
  center_layout.addStretch()
170
165
  center_layout.addWidget(self.window.ui.plugin_addon['audio.input'])
171
166
  center_layout.addWidget(self.window.ui.plugin_addon['audio.input.btn'])
172
167
  center_layout.addStretch()
173
-
174
- #right_layout = QHBoxLayout()
175
- #right_layout.addStretch(1)
176
- #right_layout.addWidget(self.window.ui.nodes['input.counter'])
177
-
178
- #grid.addLayout(left_layout, 0, 0)
179
168
  grid.addLayout(center_layout, 0, 1, alignment=Qt.AlignCenter)
180
- #grid.addLayout(right_layout, 0, 2, alignment=Qt.AlignRight)
181
169
 
182
170
  grid.setContentsMargins(0, 0, 0, 0)
183
171
  return grid
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.01.16 01:00:00 #
9
+ # Updated Date: 2025.01.17 02:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from .about import About
@@ -50,15 +50,16 @@ class Menu:
50
50
  self.audio.setup()
51
51
  self.video.setup()
52
52
  self.config.setup()
53
- self.about.setup()
54
53
 
55
54
  def post_setup(self):
56
55
  """Post setup menus"""
57
56
  # tools menu
58
57
  self.tools.setup()
59
- self.donate.setup()
58
+ self.about.setup()
60
59
 
61
60
  # debug menu
62
61
  show = self.window.core.config.get('debug')
63
62
  self.debug.setup()
64
63
  self.window.ui.menu['menu.debug'].menuAction().setVisible(show)
64
+
65
+ self.donate.setup()
@@ -6,14 +6,17 @@
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.17 02: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, QIcon
14
+ from PySide6.QtWidgets import QLabel, QHBoxLayout, QWidget, QPushButton, QVBoxLayout
14
15
 
15
16
  from pygpt_net.core.events import Event, AppEvent
17
+ from pygpt_net.ui.widget.option.toggle_label import ToggleLabel
16
18
  from pygpt_net.utils import trans
19
+ import pygpt_net.icons_rc
17
20
 
18
21
  class VoiceControlButton(QWidget):
19
22
  def __init__(self, window=None):
@@ -25,20 +28,24 @@ class VoiceControlButton(QWidget):
25
28
  super(VoiceControlButton, self).__init__(window)
26
29
  self.window = window
27
30
 
28
- self.btn_toggle = QPushButton(trans('audio.control.btn'))
31
+ self.btn_toggle = QPushButton(QIcon(":/icons/mic.svg"), trans('audio.control.btn'))
29
32
  self.btn_toggle.clicked.connect(self.toggle_recording)
30
33
  self.btn_toggle.setToolTip(trans('audio.speak.btn.tooltip'))
31
34
  self.btn_toggle.setCursor(Qt.PointingHandCursor)
32
35
  self.btn_toggle.setMinimumWidth(200)
33
36
 
37
+ self.bar = LevelBar(self)
38
+ self.bar.setLevel(0)
39
+
34
40
  # status
35
41
  self.status = QLabel("")
36
42
  self.status.setStyleSheet("color: #999; font-size: 10px; font-weight: 400; margin: 0; padding: 0; border: 0;")
37
43
  self.status.setMaximumHeight(15)
38
44
 
39
- self.layout = QHBoxLayout(self)
45
+ self.layout = QVBoxLayout(self)
40
46
  self.layout.addWidget(self.btn_toggle)
41
- self.layout.addWidget(self.status)
47
+ # self.layout.addWidget(self.status)
48
+ self.layout.addWidget(self.bar)
42
49
 
43
50
  # self.layout.addWidget(self.stop)
44
51
  self.layout.setAlignment(Qt.AlignCenter)
@@ -76,25 +83,43 @@ class AudioInputButton(QWidget):
76
83
  super(AudioInputButton, self).__init__(window)
77
84
  self.window = window
78
85
 
79
- self.btn_toggle = QPushButton(trans('audio.speak.btn'))
86
+ self.btn_toggle = QPushButton(QIcon(":/icons/mic.svg"), trans('audio.speak.btn'))
80
87
  self.btn_toggle.clicked.connect(self.toggle_recording)
81
88
  self.btn_toggle.setToolTip(trans('audio.speak.btn.tooltip'))
82
89
  self.btn_toggle.setCursor(Qt.PointingHandCursor)
83
90
  self.btn_toggle.setMinimumWidth(200)
84
91
 
85
- # 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)
92
+ self.bar = LevelBar(self)
93
+ self.bar.setLevel(0)
89
94
 
90
- self.layout = QHBoxLayout(self)
91
- self.layout.addWidget(self.btn_toggle)
92
- self.layout.addWidget(self.status)
95
+ btn_layout = QVBoxLayout()
96
+ btn_layout.addWidget(self.btn_toggle)
97
+ btn_layout.addWidget(self.bar)
98
+ btn_layout.setContentsMargins(0, 0, 0, 0)
99
+ btn_widget = QWidget()
100
+ btn_widget.setLayout(btn_layout)
101
+
102
+ self.continuous = ToggleLabel(trans('audio.speak.btn.continuous'), label_position="right")
103
+ self.continuous.box.stateChanged.connect(
104
+ lambda: self.window.controller.audio.toggle_continuous(
105
+ self.continuous.box.isChecked())
106
+ )
107
+
108
+ self.notepad_layout = QHBoxLayout()
109
+ self.notepad_layout.addWidget(self.continuous)
110
+ self.notepad_layout.setContentsMargins(0, 0, 0, 0)
111
+
112
+ self.notepad_footer = QWidget()
113
+ self.notepad_footer.setLayout(self.notepad_layout)
114
+
115
+ self.layout = QHBoxLayout()
116
+ self.layout.addWidget(btn_widget)
117
+ self.layout.addWidget(self.notepad_footer)
118
+ self.layout.setContentsMargins(0, 0, 0, 0)
93
119
 
94
- # self.layout.addWidget(self.stop)
95
- self.layout.setAlignment(Qt.AlignCenter)
96
120
  self.setLayout(self.layout)
97
- self.setMaximumHeight(80)
121
+ btn_widget.setMaximumHeight(80)
122
+ self.setMaximumHeight(120)
98
123
 
99
124
  def add_widget(self, widget):
100
125
  """
@@ -104,15 +129,50 @@ class AudioInputButton(QWidget):
104
129
  """
105
130
  self.layout.addWidget(widget)
106
131
 
107
- def set_status(self, text):
108
- """
109
- Set status text
110
-
111
- :param text: text
112
- """
113
- self.status.setText(text)
114
-
115
132
  def toggle_recording(self):
116
133
  """Toggle recording"""
117
134
  event = Event(Event.AUDIO_INPUT_RECORD_TOGGLE)
118
135
  self.window.dispatch(event)
136
+
137
+
138
+ class LevelBar(QWidget):
139
+ def __init__(self, parent=None):
140
+ super().__init__(parent)
141
+ self._level = 0.0 # level from 0.0 to 100.0
142
+ self.setFixedSize(200, 5) # bar size
143
+
144
+ def setLevel(self, level):
145
+ """
146
+ Set volume level
147
+
148
+ :param level: level
149
+ """
150
+ self._level = level
151
+ self.update()
152
+
153
+ def paintEvent(self, event):
154
+ """
155
+ Paint event
156
+
157
+ :param event: event
158
+ """
159
+ painter = QPainter(self)
160
+ painter.fillRect(self.rect(), Qt.transparent)
161
+ level_width = (self._level / 100.0) * self.width()
162
+ painter.setBrush(Qt.green)
163
+ painter.setPen(Qt.NoPen)
164
+ painter.drawRect(0, 0, level_width, self.height())
165
+
166
+ """
167
+ # --- bar from center ---
168
+ def paintEvent(self, event):
169
+ painter = QPainter(self)
170
+ painter.fillRect(self.rect(), Qt.transparent)
171
+ level_width = (self._level / 100.0) * self.width()
172
+ half_level_width = level_width / 2
173
+ center_x = self.width() / 2
174
+ rect_x = center_x - half_level_width
175
+ painter.setBrush(Qt.green)
176
+ painter.setPen(Qt.NoPen)
177
+ painter.drawRect(rect_x, 0, level_width, self.height())
178
+ """
@@ -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.51
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.51** | build: **2025.01.17** | 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,16 @@ may consume additional tokens that are not displayed in the main window.
4043
4044
 
4044
4045
  ## Recent changes:
4045
4046
 
4047
+ **2.4.51 (2025-01-17)**
4048
+
4049
+ - Added a "Continuous recording" mode under Audio Input in the Notepad tab, allowing for recording long voice notes and real-time auto-transcription. (beta)
4050
+ - A new option has been added in Settings -> Audio -> Continuous recording auto-transcribe interval.
4051
+
4052
+ **2.4.50 (2025-01-16)**
4053
+
4054
+ - Refactored audio input core.
4055
+ - Added audio input volume progress bar.
4056
+
4046
4057
  **2.4.49 (2025-01-16)**
4047
4058
 
4048
4059
  - Fix: stream render in Assistants mode.
@@ -4068,67 +4079,6 @@ may consume additional tokens that are not displayed in the main window.
4068
4079
  - Introduced a new mode in "Chat with Files": "Retrieve Only", which allows for retrieving raw documents from the index.
4069
4080
  - Fixed a bug related to tool calls in the Gemini provider when using Chat with Files mode.
4070
4081
 
4071
- **2.4.45 (2024-12-16)**
4072
-
4073
- - Enhanced web data loaders UI.
4074
-
4075
- **2.4.44 (2024-12-16)**
4076
-
4077
- - Enhanced web data loaders.
4078
- - Web loaders have been added to attachments, allowing external web content to be attached to context via the "+Web" button in the Attachments tab.
4079
- - Improved handling of attachments in groups and added an attachment icon when a group contains attachments.
4080
-
4081
- **2.4.43 (2024-12-15)**
4082
-
4083
- - Fix: Bug on attachment upload.
4084
- - Added: Attachments uploaded in groups are now available for all contexts in the group (beta).
4085
-
4086
- **2.4.42 (2024-12-15)**
4087
-
4088
- - Added Mailer plugin, which allows sending and retrieving emails from the server, and reading them. It currently supports only SMTP.
4089
- - Added 'web_request' command to the Web Search plugin, enabling GET/POST/PUT and other connections to any address and API endpoint. It also supports sending POST data, files, headers, cookies, and more.
4090
- - Improved audio output.
4091
- - Enhanced visibility of the Video menu.
4092
- - Other fixes.
4093
-
4094
- **2.4.41 (2024-12-14)**
4095
-
4096
- - Improved switching between columns on a split screen.
4097
- - Added visual identification of the active column.
4098
-
4099
- **2.4.40 (2024-12-13)**
4100
-
4101
- - Enhanced Split Screen mode, now promoted from beta to stable.
4102
- - Python Code Interpreter tool added to the Tabs.
4103
- - HTML/JS Canvas tool added to the Tabs.
4104
- - Added attachment icon to the context list if context has attachments.
4105
- - Improved audio playback.
4106
- - Improved web search.
4107
- - Added a thumbnail image to web search results.
4108
- - Added a new commands to web search: "extract_images" and "extract_links".
4109
- - Added the option "Use raw content (without summarization)" to the web search plugin, which provides a more detailed result to the main model.
4110
- - Extended the default maximum result characters to 50,000 in the web search plugin.
4111
-
4112
- **2.4.39 (2024-12-09)**
4113
-
4114
- - Added "Split Screen" mode (accessible via the switch in the bottom-right corner of the screen), which allows you to work in two windows simultaneously. It is currently experimental (beta). Future updates will include Code Interpreter and Canvas running in tabs.
4115
-
4116
- - Fixed: Language switch.
4117
-
4118
- **2.4.38 (2024-12-08)**
4119
-
4120
- - Added the ability to select a style for chat display between: Blocks, ChatGPT-like, and ChatGPT-like Wide. New option in the menu: Config -> Theme -> Style...
4121
- - Added configuration options for audio input in Settings -> Audio -> Audio Input Device, Channels, and Sampling rate.
4122
-
4123
- **2.4.37 (2024-11-30)**
4124
-
4125
- - The `Query only` mode in `Uploaded` tab has been renamed to `RAG`.
4126
- - New options have been added under `Settings -> Files and Attachments`:
4127
- - `Use history in RAG query`: When enabled, the content of the entire conversation will be used when preparing a query if the mode is set to RAG or Summary.
4128
- - `RAG limit`: This option is applicable only if 'Use history in RAG query' is enabled. It specifies the limit on how many recent entries in the conversation will be used when generating a query for RAG. A value of 0 indicates no limit.
4129
- - Cache: dynamic parts of the system prompt (from plugins) have been moved to the very end of the prompt stack to enable the use of prompt cache mechanisms in OpenAI.
4130
-
4131
-
4132
4082
  # Credits and links
4133
4083
 
4134
4084
  **Official website:** <https://pygpt.net>