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.
- pygpt_net/CHANGELOG.txt +7 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +3 -1
- pygpt_net/app_core.py +3 -1
- pygpt_net/config.py +3 -1
- pygpt_net/controller/__init__.py +5 -1
- pygpt_net/controller/audio/audio.py +13 -0
- pygpt_net/controller/chat/common.py +18 -83
- pygpt_net/controller/lang/custom.py +2 -2
- pygpt_net/controller/media/__init__.py +12 -0
- pygpt_net/controller/media/media.py +115 -0
- pygpt_net/controller/realtime/realtime.py +27 -2
- pygpt_net/controller/ui/mode.py +16 -2
- pygpt_net/core/audio/backend/pyaudio/realtime.py +51 -14
- pygpt_net/core/audio/output.py +3 -2
- pygpt_net/core/image/image.py +6 -5
- pygpt_net/core/realtime/worker.py +1 -5
- pygpt_net/core/render/web/body.py +24 -3
- pygpt_net/core/text/utils.py +54 -2
- pygpt_net/core/types/image.py +7 -1
- pygpt_net/core/video/__init__.py +12 -0
- pygpt_net/core/video/video.py +290 -0
- pygpt_net/data/config/config.json +19 -4
- pygpt_net/data/config/models.json +75 -3
- pygpt_net/data/config/settings.json +194 -6
- pygpt_net/data/css/web-blocks.css +6 -0
- pygpt_net/data/css/web-chatgpt.css +6 -0
- pygpt_net/data/css/web-chatgpt_wide.css +6 -0
- pygpt_net/data/locale/locale.de.ini +30 -2
- pygpt_net/data/locale/locale.en.ini +40 -7
- pygpt_net/data/locale/locale.es.ini +30 -2
- pygpt_net/data/locale/locale.fr.ini +30 -2
- pygpt_net/data/locale/locale.it.ini +30 -2
- pygpt_net/data/locale/locale.pl.ini +33 -2
- pygpt_net/data/locale/locale.uk.ini +30 -2
- pygpt_net/data/locale/locale.zh.ini +30 -2
- pygpt_net/data/locale/plugin.cmd_web.en.ini +8 -0
- pygpt_net/item/model.py +22 -1
- pygpt_net/provider/api/google/__init__.py +38 -2
- pygpt_net/provider/api/google/video.py +364 -0
- pygpt_net/provider/api/openai/realtime/realtime.py +1 -2
- pygpt_net/provider/core/config/patch.py +226 -178
- pygpt_net/provider/core/model/patch.py +17 -2
- pygpt_net/provider/web/duckduck_search.py +212 -0
- pygpt_net/ui/layout/toolbox/audio.py +55 -0
- pygpt_net/ui/layout/toolbox/footer.py +14 -58
- pygpt_net/ui/layout/toolbox/image.py +3 -14
- pygpt_net/ui/layout/toolbox/raw.py +52 -0
- pygpt_net/ui/layout/toolbox/split.py +48 -0
- pygpt_net/ui/layout/toolbox/toolbox.py +8 -8
- pygpt_net/ui/layout/toolbox/video.py +49 -0
- {pygpt_net-2.6.31.dist-info → pygpt_net-2.6.32.dist-info}/METADATA +23 -11
- {pygpt_net-2.6.31.dist-info → pygpt_net-2.6.32.dist-info}/RECORD +56 -46
- {pygpt_net-2.6.31.dist-info → pygpt_net-2.6.32.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.31.dist-info → pygpt_net-2.6.32.dist-info}/WHEEL +0 -0
- {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.
|
|
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.
|
|
17
|
-
__build__ = "2025-09-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
pygpt_net/controller/__init__.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.
|
|
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.
|
|
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
|
|
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
|
|
395
|
+
Toggle raw (plain) output
|
|
460
396
|
|
|
461
|
-
:param
|
|
397
|
+
:param value: value of the checkbox
|
|
462
398
|
"""
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
473
|
-
|
|
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
|
|
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
|
"""
|
pygpt_net/controller/ui/mode.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.
|
|
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['
|
|
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
|
-
|
|
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.
|
pygpt_net/core/audio/output.py
CHANGED
|
@@ -43,8 +43,9 @@ class Output:
|
|
|
43
43
|
return self.backends[backend]
|
|
44
44
|
|
|
45
45
|
def setup(self):
|
|
46
|
-
"""Setup audio
|
|
47
|
-
|
|
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,
|