pygpt-net 2.6.31__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.
Files changed (56) hide show
  1. pygpt_net/CHANGELOG.txt +7 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +3 -1
  4. pygpt_net/app_core.py +3 -1
  5. pygpt_net/config.py +3 -1
  6. pygpt_net/controller/__init__.py +5 -1
  7. pygpt_net/controller/audio/audio.py +13 -0
  8. pygpt_net/controller/chat/common.py +18 -83
  9. pygpt_net/controller/lang/custom.py +2 -2
  10. pygpt_net/controller/media/__init__.py +12 -0
  11. pygpt_net/controller/media/media.py +115 -0
  12. pygpt_net/controller/realtime/realtime.py +27 -2
  13. pygpt_net/controller/ui/mode.py +16 -2
  14. pygpt_net/core/audio/backend/pyaudio/realtime.py +51 -14
  15. pygpt_net/core/audio/output.py +3 -2
  16. pygpt_net/core/image/image.py +6 -5
  17. pygpt_net/core/realtime/worker.py +1 -5
  18. pygpt_net/core/render/web/body.py +24 -3
  19. pygpt_net/core/text/utils.py +54 -2
  20. pygpt_net/core/types/image.py +7 -1
  21. pygpt_net/core/video/__init__.py +12 -0
  22. pygpt_net/core/video/video.py +290 -0
  23. pygpt_net/data/config/config.json +19 -4
  24. pygpt_net/data/config/models.json +75 -3
  25. pygpt_net/data/config/settings.json +194 -6
  26. pygpt_net/data/css/web-blocks.css +6 -0
  27. pygpt_net/data/css/web-chatgpt.css +6 -0
  28. pygpt_net/data/css/web-chatgpt_wide.css +6 -0
  29. pygpt_net/data/locale/locale.de.ini +30 -2
  30. pygpt_net/data/locale/locale.en.ini +40 -7
  31. pygpt_net/data/locale/locale.es.ini +30 -2
  32. pygpt_net/data/locale/locale.fr.ini +30 -2
  33. pygpt_net/data/locale/locale.it.ini +30 -2
  34. pygpt_net/data/locale/locale.pl.ini +33 -2
  35. pygpt_net/data/locale/locale.uk.ini +30 -2
  36. pygpt_net/data/locale/locale.zh.ini +30 -2
  37. pygpt_net/data/locale/plugin.cmd_web.en.ini +8 -0
  38. pygpt_net/item/model.py +22 -1
  39. pygpt_net/provider/api/google/__init__.py +38 -2
  40. pygpt_net/provider/api/google/video.py +364 -0
  41. pygpt_net/provider/api/openai/realtime/realtime.py +1 -2
  42. pygpt_net/provider/core/config/patch.py +226 -178
  43. pygpt_net/provider/core/model/patch.py +17 -2
  44. pygpt_net/provider/web/duckduck_search.py +212 -0
  45. pygpt_net/ui/layout/toolbox/audio.py +55 -0
  46. pygpt_net/ui/layout/toolbox/footer.py +14 -58
  47. pygpt_net/ui/layout/toolbox/image.py +3 -14
  48. pygpt_net/ui/layout/toolbox/raw.py +52 -0
  49. pygpt_net/ui/layout/toolbox/split.py +48 -0
  50. pygpt_net/ui/layout/toolbox/toolbox.py +8 -8
  51. pygpt_net/ui/layout/toolbox/video.py +49 -0
  52. {pygpt_net-2.6.31.dist-info → pygpt_net-2.6.32.dist-info}/METADATA +23 -11
  53. {pygpt_net-2.6.31.dist-info → pygpt_net-2.6.32.dist-info}/RECORD +56 -46
  54. {pygpt_net-2.6.31.dist-info → pygpt_net-2.6.32.dist-info}/LICENSE +0 -0
  55. {pygpt_net-2.6.31.dist-info → pygpt_net-2.6.32.dist-info}/WHEEL +0 -0
  56. {pygpt_net-2.6.31.dist-info → pygpt_net-2.6.32.dist-info}/entry_points.txt +0 -0
pygpt_net/CHANGELOG.txt CHANGED
@@ -1,3 +1,10 @@
1
+ 2.6.32 (2025-09-02)
2
+
3
+ - Added video generation and support for Google Veo 3 models.
4
+ - Introduced new predefined models: veo-3.0-generate-preview and veo-3.0-fast-generate-preview.
5
+ - Integrated DuckDuckGo as a search provider in the WebSearch plugin.
6
+ - Added "Loop" mode to Realtime + audio mode for automatic turn handling and continuous conversation without manually enabling the microphone.
7
+
1
8
  2.6.31 (2025-09-01)
2
9
 
3
10
  - Chat with Audio mode renamed to Realtime + audio.
pygpt_net/__init__.py CHANGED
@@ -6,15 +6,15 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.01 00:00:00 #
9
+ # Updated Date: 2025.09.02 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  __author__ = "Marcin Szczygliński"
13
13
  __copyright__ = "Copyright 2025, Marcin Szczygliński"
14
14
  __credits__ = ["Marcin Szczygliński"]
15
15
  __license__ = "MIT"
16
- __version__ = "2.6.31"
17
- __build__ = "2025-09-01"
16
+ __version__ = "2.6.32"
17
+ __build__ = "2025-09-02"
18
18
  __maintainer__ = "Marcin Szczygliński"
19
19
  __github__ = "https://github.com/szczyglis-dev/py-gpt"
20
20
  __report__ = "https://github.com/szczyglis-dev/py-gpt/issues"
pygpt_net/app.py CHANGED
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.27 20:00:00 #
9
+ # Updated Date: 2025.09.02 01:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -182,6 +182,7 @@ from pygpt_net.provider.audio_output.eleven_labs import ElevenLabsTextToSpeech
182
182
  # web search engine providers
183
183
  from pygpt_net.provider.web.google_custom_search import GoogleCustomSearch
184
184
  from pygpt_net.provider.web.microsoft_bing import MicrosoftBingSearch
185
+ from pygpt_net.provider.web.duckduck_search import DuckDuckGoSearch
185
186
 
186
187
  # tools
187
188
  from pygpt_net.tools.indexer import IndexerTool
@@ -342,6 +343,7 @@ def run(**kwargs):
342
343
  # register web providers
343
344
  launcher.add_web(GoogleCustomSearch())
344
345
  launcher.add_web(MicrosoftBingSearch())
346
+ launcher.add_web(DuckDuckGoSearch())
345
347
 
346
348
  # register custom web providers
347
349
  providers = kwargs.get('web', None)
pygpt_net/app_core.py CHANGED
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.28 09:00:00 #
9
+ # Updated Date: 2025.09.01 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from pygpt_net.config import Config
@@ -43,6 +43,7 @@ from pygpt_net.core.tabs import Tabs
43
43
  from pygpt_net.core.text import Text
44
44
  from pygpt_net.core.tokens import Tokens
45
45
  from pygpt_net.core.updater import Updater
46
+ from pygpt_net.core.video import Video
46
47
  from pygpt_net.core.vision import Vision
47
48
  from pygpt_net.core.web import Web
48
49
 
@@ -92,6 +93,7 @@ class Core:
92
93
  self.text = Text(window)
93
94
  self.tokens = Tokens(window)
94
95
  self.updater = Updater(window)
96
+ self.video = Video(window)
95
97
  self.vision = Vision(window)
96
98
  self.web = Web(window)
97
99
 
pygpt_net/config.py CHANGED
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.18 01:00:00 #
9
+ # Updated Date: 2025.09.01 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
@@ -66,6 +66,8 @@ class Config:
66
66
  "presets": "presets",
67
67
  "upload": "upload",
68
68
  "tmp": "tmp",
69
+ "video": "video",
70
+ "music": "music",
69
71
  }
70
72
  self._app_path = None
71
73
  self._version_cache = version if version else 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.30 06:00:00 #
9
+ # Updated Date: 2025.09.01 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from pygpt_net.controller.access import Access
@@ -29,6 +29,7 @@ from pygpt_net.controller.kernel import Kernel
29
29
  from pygpt_net.controller.lang import Lang
30
30
  from pygpt_net.controller.launcher import Launcher
31
31
  from pygpt_net.controller.layout import Layout
32
+ from pygpt_net.controller.media import Media
32
33
  from pygpt_net.controller.mode import Mode
33
34
  from pygpt_net.controller.model import Model
34
35
  from pygpt_net.controller.notepad import Notepad
@@ -71,6 +72,7 @@ class Controller:
71
72
  self.lang = Lang(window)
72
73
  self.launcher = Launcher(window)
73
74
  self.layout = Layout(window)
75
+ self.media = Media(window)
74
76
  self.mode = Mode(window)
75
77
  self.model = Model(window)
76
78
  self.notepad = Notepad(window)
@@ -111,6 +113,7 @@ class Controller:
111
113
  self.camera.setup_ui()
112
114
  self.access.setup()
113
115
  self.realtime.setup()
116
+ self.media.setup()
114
117
 
115
118
  def post_setup(self):
116
119
  """Post-setup, after plugins are loaded"""
@@ -169,6 +172,7 @@ class Controller:
169
172
  self.lang.reload()
170
173
  self.debug.reload()
171
174
  self.chat.reload()
175
+ self.media.reload()
172
176
  self.window.tools.on_reload()
173
177
  self.access.reload()
174
178
  self.tools.reload()
@@ -40,12 +40,19 @@ class Audio:
40
40
  def setup(self):
41
41
  """Setup controller"""
42
42
  self.update()
43
+
44
+ # continuous input (notepad)
43
45
  if self.window.core.config.get("audio.input.continuous", False):
44
46
  self.window.ui.plugin_addon['audio.input.btn'].continuous.setChecked(True)
45
47
 
48
+ # auto turn (VAD)
46
49
  if self.window.core.config.get("audio.input.auto_turn", False):
47
50
  self.window.ui.nodes['audio.auto_turn'].box.setChecked(True)
48
51
 
52
+ # loop recording
53
+ if self.window.core.config.get("audio.input.loop", False):
54
+ self.window.ui.nodes['audio.loop'].box.setChecked(True)
55
+
49
56
  def execute_input_stop(self):
50
57
  """Execute input stop (from UI)"""
51
58
  self.window.dispatch(Event(Event.AUDIO_INPUT_RECORD_TOGGLE, {
@@ -67,6 +74,12 @@ class Audio:
67
74
  self.window.core.config.set("audio.input.auto_turn", value)
68
75
  self.window.core.config.save()
69
76
 
77
+ def toggle_loop(self):
78
+ """Toggle loop recording setting"""
79
+ value = self.window.ui.nodes['audio.loop'].box.isChecked()
80
+ self.window.core.config.set("audio.input.loop", value)
81
+ self.window.core.config.save()
82
+
70
83
  def toggle_input(
71
84
  self,
72
85
  state: bool,
@@ -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.27 07:00:00 #
9
+ # Updated Date: 2025.09.01 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -100,37 +100,6 @@ class Common:
100
100
  event = RenderEvent(RenderEvent.ON_SWITCH)
101
101
  self.window.dispatch(event) # switch renderer if needed
102
102
 
103
- # edit icons
104
- """
105
- if self.window.core.config.has('ctx.edit_icons'):
106
- self.window.ui.nodes['output.edit'].setChecked(self.window.core.config.get('ctx.edit_icons'))
107
- data = {
108
- "initialized": self.initialized,
109
- }
110
- if self.window.core.config.get('ctx.edit_icons'):
111
- event = RenderEvent(RenderEvent.ON_EDIT_ENABLE, data)
112
- else:
113
- event = RenderEvent(RenderEvent.ON_EDIT_DISABLE, data)
114
- self.window.dispatch(event)
115
- """
116
-
117
- # images generation
118
- if self.window.core.config.get('img_raw'):
119
- self.window.ui.config['global']['img_raw'].setChecked(True)
120
- else:
121
- self.window.ui.config['global']['img_raw'].setChecked(False)
122
-
123
- # image resolution
124
- resolution = self.window.core.config.get('img_resolution', '1024x1024')
125
- self.window.controller.config.apply_value(
126
- parent_id="global",
127
- key="img_resolution",
128
- option=self.window.core.image.get_resolution_option(),
129
- value=resolution,
130
- )
131
- if not self.initialized:
132
- self.window.ui.add_hook("update.global.img_resolution", self.hook_update)
133
-
134
103
  # set focus to input
135
104
  self.window.ui.nodes['input'].setFocus()
136
105
  self.initialized = True
@@ -404,29 +373,6 @@ class Common:
404
373
  event = RenderEvent(RenderEvent.ON_TS_DISABLE, data)
405
374
  self.window.dispatch(event)
406
375
 
407
- def toggle_raw(self, value: bool):
408
- """
409
- Toggle raw (plain) output
410
-
411
- :param value: value of the checkbox
412
- """
413
- self.window.core.config.set('render.plain', value)
414
- self.window.core.config.save()
415
-
416
- # update checkbox in settings dialog
417
- self.window.controller.config.checkbox.apply(
418
- 'config',
419
- 'render.plain',
420
- {
421
- 'value': value
422
- },
423
- )
424
- event = RenderEvent(RenderEvent.ON_SWITCH)
425
- self.window.dispatch(event)
426
-
427
- # restore previous font size
428
- self.window.controller.ui.update_font_size()
429
-
430
376
  def toggle_edit_icons(self, value: bool):
431
377
  """
432
378
  Toggle edit icons
@@ -444,39 +390,28 @@ class Common:
444
390
  event = RenderEvent(RenderEvent.ON_EDIT_DISABLE, data)
445
391
  self.window.dispatch(event)
446
392
 
447
- def img_enable_raw(self):
448
- """Enable help for images"""
449
- self.window.core.config.set('img_raw', True)
450
- self.window.core.config.save()
451
-
452
- def img_disable_raw(self):
453
- """Disable help for images"""
454
- self.window.core.config.set('img_raw', False)
455
- self.window.core.config.save()
456
-
457
- def img_toggle_raw(self, state: bool):
393
+ def toggle_raw(self, value: bool):
458
394
  """
459
- Toggle help for images
395
+ Toggle raw (plain) output
460
396
 
461
- :param state: state of checkbox
397
+ :param value: value of the checkbox
462
398
  """
463
- if not state:
464
- self.img_disable_raw()
465
- else:
466
- self.img_enable_raw()
399
+ self.window.core.config.set('render.plain', value)
400
+ self.window.core.config.save()
467
401
 
468
- def hook_update(self, key: str, value: Any, caller, *args, **kwargs):
469
- """
470
- Hook for updating image resolution
402
+ # update checkbox in settings dialog
403
+ self.window.controller.config.checkbox.apply(
404
+ 'config',
405
+ 'render.plain',
406
+ {
407
+ 'value': value
408
+ },
409
+ )
410
+ event = RenderEvent(RenderEvent.ON_SWITCH)
411
+ self.window.dispatch(event)
471
412
 
472
- :param key: config key
473
- :param value: new value
474
- :param caller: caller object
475
- """
476
- if key == "img_resolution":
477
- if not value:
478
- return
479
- self.window.core.config.set('img_resolution', value)
413
+ # restore previous font size
414
+ self.window.controller.ui.update_font_size()
480
415
 
481
416
  def save_text(
482
417
  self,
@@ -55,8 +55,8 @@ class Custom:
55
55
  self.window.ui.config['preset'][MODE_CHAT].box.setText(trans("preset.chat"))
56
56
  self.window.ui.config['preset'][MODE_COMPLETION].box.setText(trans("preset.completion"))
57
57
  self.window.ui.config['preset'][MODE_IMAGE].box.setText(trans("preset.img"))
58
- self.window.ui.config['preset'][MODE_VISION].box.setText(trans("preset.vision"))
59
- #self.window.ui.config['preset'][MODE_LANGCHAIN].box.setText(trans("preset.langchain"))
58
+ # self.window.ui.config['preset'][MODE_VISION].box.setText(trans("preset.vision"))
59
+ # self.window.ui.config['preset'][MODE_LANGCHAIN].box.setText(trans("preset.langchain"))
60
60
  self.window.ui.config['preset'][MODE_LLAMA_INDEX].box.setText(trans("preset.llama_index"))
61
61
  self.window.ui.config['preset'][MODE_AGENT].box.setText(trans("preset.agent"))
62
62
  self.window.ui.config['preset'][MODE_AGENT_LLAMA].box.setText(trans("preset.agent_llama"))
@@ -0,0 +1,12 @@
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.09.01 23:00:00 #
10
+ # ================================================== #
11
+
12
+ from .media import Media
@@ -0,0 +1,115 @@
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.09.01 23:00:00 #
10
+ # ================================================== #
11
+
12
+ from typing import Any
13
+
14
+
15
+ class Media:
16
+ def __init__(self, window=None):
17
+ """
18
+ Media (video, image, music) controller
19
+
20
+ :param window: Window instance
21
+ """
22
+ self.window = window
23
+ self.initialized = False
24
+
25
+ def setup(self):
26
+ """Setup UI"""
27
+ # raw mode for images/video
28
+ if self.window.core.config.get('img_raw'):
29
+ self.window.ui.config['global']['img_raw'].setChecked(True)
30
+ else:
31
+ self.window.ui.config['global']['img_raw'].setChecked(False)
32
+
33
+ # image: resolution
34
+ resolution = self.window.core.config.get('img_resolution', '1024x1024')
35
+ self.window.controller.config.apply_value(
36
+ parent_id="global",
37
+ key="img_resolution",
38
+ option=self.window.core.image.get_resolution_option(),
39
+ value=resolution,
40
+ )
41
+
42
+ # video: aspect ratio
43
+ aspect_ratio = self.window.core.config.get('video.aspect_ratio', '16:9')
44
+ self.window.controller.config.apply_value(
45
+ parent_id="global",
46
+ key="video.aspect_ratio",
47
+ option=self.window.core.video.get_aspect_ratio_option(),
48
+ value=aspect_ratio,
49
+ )
50
+
51
+ # -- add hooks --
52
+ if not self.initialized:
53
+ self.window.ui.add_hook("update.global.img_resolution", self.hook_update)
54
+ self.window.ui.add_hook("update.global.video.aspect_ratio", self.hook_update)
55
+
56
+ def reload(self):
57
+ """Reload UI"""
58
+ self.setup()
59
+
60
+ def hook_update(self, key: str, value: Any, caller, *args, **kwargs):
61
+ """
62
+ Hook for updating media options
63
+
64
+ :param key: config key
65
+ :param value: new value
66
+ :param caller: caller object
67
+ """
68
+ if key == "img_resolution":
69
+ if not value:
70
+ return
71
+ self.window.core.config.set('img_resolution', value)
72
+ elif key == "video.aspect_ratio":
73
+ if not value:
74
+ return
75
+ self.window.core.config.set('video.aspect_ratio', value)
76
+
77
+ def enable_raw(self):
78
+ """Enable prompt enhancement for images"""
79
+ self.window.core.config.set('img_raw', True)
80
+ self.window.core.config.save()
81
+
82
+ def disable_raw(self):
83
+ """Disable prompt enhancement for images"""
84
+ self.window.core.config.set('img_raw', False)
85
+ self.window.core.config.save()
86
+
87
+ def toggle_raw(self):
88
+ """Save prompt enhancement option for images"""
89
+ state = self.window.ui.config['global']['img_raw'].isChecked()
90
+ if not state:
91
+ self.disable_raw()
92
+ else:
93
+ self.enable_raw()
94
+
95
+ def is_image_model(self) -> bool:
96
+ """
97
+ Check if the model is an image generation model
98
+
99
+ :return: True if the model is an image generation model
100
+ """
101
+ current = self.window.core.config.get("model")
102
+ model_data = self.window.core.models.get(current)
103
+ if model_data:
104
+ return model_data.is_image_output()
105
+
106
+ def is_video_model(self) -> bool:
107
+ """
108
+ Check if the model is a video generation model
109
+
110
+ :return: True if the model is a video generation model
111
+ """
112
+ current = self.window.core.config.get("model")
113
+ model_data = self.window.core.models.get(current)
114
+ if model_data:
115
+ return model_data.is_video_output()
@@ -11,7 +11,14 @@
11
11
 
12
12
  from PySide6.QtCore import Slot, QTimer
13
13
 
14
- from pygpt_net.core.events import RealtimeEvent, RenderEvent, BaseEvent, AppEvent, KernelEvent
14
+ from pygpt_net.core.events import (
15
+ RealtimeEvent,
16
+ RenderEvent,
17
+ BaseEvent,
18
+ AppEvent,
19
+ KernelEvent,
20
+ Event,
21
+ )
15
22
  from pygpt_net.core.realtime.worker import RealtimeSignals
16
23
  from pygpt_net.core.types import MODE_AUDIO
17
24
  from pygpt_net.utils import trans
@@ -122,10 +129,12 @@ class Realtime:
122
129
  "begin": False,
123
130
  }))
124
131
 
125
- # audio end: stop audio playback
132
+ # audio end: on stop audio playback
126
133
  elif event.name == RealtimeEvent.RT_OUTPUT_AUDIO_END:
127
134
  self.set_idle()
128
135
  self.window.controller.chat.common.unlock_input()
136
+ if self.is_loop():
137
+ QTimer.singleShot(500, lambda: self.next_turn()) # wait a bit before next turn
129
138
 
130
139
  # end of turn: finalize the response
131
140
  elif event.name == RealtimeEvent.RT_OUTPUT_TURN_END:
@@ -163,6 +172,22 @@ class Realtime:
163
172
  elif event.name == AppEvent.CTX_SELECTED:
164
173
  QTimer.singleShot(0, lambda: self.reset())
165
174
 
175
+ def next_turn(self):
176
+ """Start next turn in loop mode (if enabled)"""
177
+ self.window.dispatch(Event(Event.AUDIO_INPUT_RECORD_TOGGLE))
178
+ if self.window.controller.audio.is_recording():
179
+ QTimer.singleShot(100, lambda: self.window.update_status(trans("speech.listening")))
180
+
181
+ def is_loop(self) -> bool:
182
+ """
183
+ Check if loop recording is enabled
184
+
185
+ :return: True if loop recording is enabled, False otherwise
186
+ """
187
+ if self.window.controller.kernel.stopped():
188
+ return False
189
+ return self.window.core.config.get("audio.input.loop", False)
190
+
166
191
  @Slot(object)
167
192
  def handle_response(self, event: RealtimeEvent):
168
193
  """
@@ -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.15 23:00:00 #
9
+ # Updated Date: 2025.09.01 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from pygpt_net.core.types import (
@@ -60,8 +60,10 @@ class Mode:
60
60
 
61
61
  if not is_audio:
62
62
  self.window.ui.nodes['audio.auto_turn'].setVisible(False)
63
+ self.window.ui.nodes["audio.loop"].setVisible(False)
63
64
  else:
64
65
  self.window.ui.nodes['audio.auto_turn'].setVisible(True)
66
+ self.window.ui.nodes["audio.loop"].setVisible(True)
65
67
 
66
68
  if not is_assistant:
67
69
  ui_nodes['presets.widget'].setVisible(True)
@@ -138,9 +140,21 @@ class Mode:
138
140
  ui_tabs['preset.editor.extra'].setTabText(0, trans("preset.prompt"))
139
141
 
140
142
  if is_image:
141
- ui_nodes['dalle.options'].setVisible(True)
143
+ ui_nodes['media.raw'].setVisible(True)
144
+ if ctrl.media.is_video_model():
145
+ ui_nodes['video.options'].setVisible(True)
146
+ ui_nodes['dalle.options'].setVisible(False)
147
+ elif ctrl.media.is_image_model():
148
+ ui_nodes['dalle.options'].setVisible(True)
149
+ ui_nodes['video.options'].setVisible(False)
150
+ else:
151
+ ui_nodes['media.raw'].setVisible(False)
152
+ ui_nodes['dalle.options'].setVisible(False)
153
+ ui_nodes['video.options'].setVisible(False)
142
154
  else:
155
+ ui_nodes['media.raw'].setVisible(False)
143
156
  ui_nodes['dalle.options'].setVisible(False)
157
+ ui_nodes['video.options'].setVisible(False)
144
158
 
145
159
  if is_agent:
146
160
  ui_nodes['agent.options'].setVisible(True)
@@ -1,14 +1,3 @@
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.31 23:00:00 #
10
- # ================================================== #
11
-
12
1
  import threading
13
2
  from typing import Optional
14
3
 
@@ -53,6 +42,9 @@ class RealtimeSessionPyAudio(QObject):
53
42
  self._final = False
54
43
  self._tail_ms = 60 # add a small silence tail to avoid clicks
55
44
 
45
+ # one-shot guard to avoid double stop and duplicate callbacks
46
+ self._stopping = False
47
+
56
48
  # volume metering
57
49
  self._volume_emitter = volume_emitter
58
50
  self._vol_buffer = bytearray()
@@ -78,6 +70,13 @@ class RealtimeSessionPyAudio(QObject):
78
70
  except Exception:
79
71
  pass
80
72
 
73
+ # finished-state watchdog: guarantees stop()+on_stopped once playback is truly done
74
+ self._finish_timer = QTimer(self)
75
+ self._finish_timer.setTimerType(Qt.PreciseTimer)
76
+ self._finish_timer.setInterval(15) # fast but lightweight watchdog
77
+ self._finish_timer.timeout.connect(self._check_finished)
78
+ self._finish_timer.start()
79
+
81
80
  # stop callback (set by backend)
82
81
  self.on_stopped = None
83
82
 
@@ -124,15 +123,28 @@ class RealtimeSessionPyAudio(QObject):
124
123
  self._final = True
125
124
 
126
125
  def stop(self) -> None:
127
- """Stop playback and free resources."""
126
+ """Stop playback and free resources. Idempotent."""
127
+ # ensure this executes only once even if called from multiple paths
128
+ if self._stopping:
129
+ return
130
+ self._stopping = True
131
+
132
+ # stop timers first to prevent re-entry
133
+ try:
134
+ if self._finish_timer:
135
+ self._finish_timer.stop()
136
+ except Exception:
137
+ pass
128
138
  try:
129
139
  if self._vol_timer:
130
140
  self._vol_timer.stop()
131
141
  except Exception:
132
142
  pass
143
+
144
+ # gracefully stop PortAudio stream and close/terminate
133
145
  try:
134
146
  if self._stream and self._stream.is_active():
135
- self._stream.stop_stream()
147
+ self._stream.stop_stream() # drains queued audio per PortAudio docs
136
148
  except Exception:
137
149
  pass
138
150
  try:
@@ -197,11 +209,36 @@ class RealtimeSessionPyAudio(QObject):
197
209
 
198
210
  # auto-finish: when final and nothing more to play, complete and stop()
199
211
  if self._final and self._buffer_empty():
200
- QTimer.singleShot(0, self.stop) # stop on the GUI thread
212
+ # Return paComplete and request stop on the GUI thread.
213
+ # PaComplete deactivates the stream after the last callback buffer is played.
214
+ QTimer.singleShot(0, self.stop)
201
215
  return out, pyaudio.paComplete
202
216
 
203
217
  return out, pyaudio.paContinue
204
218
 
219
+ def _check_finished(self) -> None:
220
+ """
221
+ Watchdog that runs on the Qt thread to guarantee a single, reliable stop().
222
+ Triggers when PortAudio deactivates the stream, or when the buffer is fully
223
+ drained after mark_final().
224
+ """
225
+ if self._stopping:
226
+ return
227
+
228
+ # If underlying PA stream is no longer active, we are done.
229
+ try:
230
+ if self._stream is not None and not self._stream.is_active():
231
+ self.stop()
232
+ return
233
+ except Exception:
234
+ # If querying state fails, assume the stream is done and stop.
235
+ self.stop()
236
+ return
237
+
238
+ # If we've been marked final and our buffer is empty, finalize proactively.
239
+ if self._final and self._buffer_empty():
240
+ self.stop()
241
+
205
242
  def _buffer_empty(self) -> bool:
206
243
  """
207
244
  Check if internal buffer is empty.
@@ -43,8 +43,9 @@ class Output:
43
43
  return self.backends[backend]
44
44
 
45
45
  def setup(self):
46
- """Setup audio output backend"""
47
- pass
46
+ """Setup audio input backend"""
47
+ for b in self.backends.values():
48
+ b.set_rt_signals(self.window.controller.realtime.signals)
48
49
 
49
50
  def play(
50
51
  self,