pygpt-net 2.7.7__py3-none-any.whl → 2.7.8__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 (93) hide show
  1. pygpt_net/CHANGELOG.txt +7 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +5 -1
  4. pygpt_net/controller/assistant/batch.py +2 -2
  5. pygpt_net/controller/assistant/files.py +7 -6
  6. pygpt_net/controller/assistant/threads.py +0 -0
  7. pygpt_net/controller/chat/command.py +0 -0
  8. pygpt_net/controller/dialogs/confirm.py +35 -58
  9. pygpt_net/controller/lang/mapping.py +9 -9
  10. pygpt_net/controller/remote_store/{google/batch.py → batch.py} +209 -252
  11. pygpt_net/controller/remote_store/remote_store.py +982 -13
  12. pygpt_net/core/command/command.py +0 -0
  13. pygpt_net/core/db/viewer.py +1 -1
  14. pygpt_net/core/realtime/worker.py +3 -1
  15. pygpt_net/{controller/remote_store/google → core/remote_store/anthropic}/__init__.py +0 -1
  16. pygpt_net/core/remote_store/anthropic/files.py +211 -0
  17. pygpt_net/core/remote_store/anthropic/store.py +208 -0
  18. pygpt_net/core/remote_store/openai/store.py +5 -4
  19. pygpt_net/core/remote_store/remote_store.py +5 -1
  20. pygpt_net/{controller/remote_store/openai → core/remote_store/xai}/__init__.py +0 -1
  21. pygpt_net/core/remote_store/xai/files.py +225 -0
  22. pygpt_net/core/remote_store/xai/store.py +219 -0
  23. pygpt_net/data/config/config.json +9 -6
  24. pygpt_net/data/config/models.json +5 -4
  25. pygpt_net/data/config/settings.json +54 -1
  26. pygpt_net/data/icons/folder_eye.svg +1 -0
  27. pygpt_net/data/icons/folder_eye_filled.svg +1 -0
  28. pygpt_net/data/icons/folder_open.svg +1 -0
  29. pygpt_net/data/icons/folder_open_filled.svg +1 -0
  30. pygpt_net/data/locale/locale.de.ini +4 -3
  31. pygpt_net/data/locale/locale.en.ini +14 -4
  32. pygpt_net/data/locale/locale.es.ini +4 -3
  33. pygpt_net/data/locale/locale.fr.ini +4 -3
  34. pygpt_net/data/locale/locale.it.ini +4 -3
  35. pygpt_net/data/locale/locale.pl.ini +5 -4
  36. pygpt_net/data/locale/locale.uk.ini +4 -3
  37. pygpt_net/data/locale/locale.zh.ini +4 -3
  38. pygpt_net/icons.qrc +4 -0
  39. pygpt_net/icons_rc.py +282 -138
  40. pygpt_net/provider/api/anthropic/__init__.py +2 -0
  41. pygpt_net/provider/api/anthropic/chat.py +84 -1
  42. pygpt_net/provider/api/anthropic/store.py +307 -0
  43. pygpt_net/provider/api/anthropic/stream.py +75 -0
  44. pygpt_net/provider/api/anthropic/worker/__init__.py +0 -0
  45. pygpt_net/provider/api/anthropic/worker/importer.py +278 -0
  46. pygpt_net/provider/api/google/chat.py +59 -2
  47. pygpt_net/provider/api/google/store.py +124 -3
  48. pygpt_net/provider/api/google/stream.py +91 -24
  49. pygpt_net/provider/api/google/worker/importer.py +16 -28
  50. pygpt_net/provider/api/openai/assistants.py +2 -2
  51. pygpt_net/provider/api/openai/store.py +4 -1
  52. pygpt_net/provider/api/openai/worker/importer.py +19 -61
  53. pygpt_net/provider/api/openai/worker/importer_assistants.py +230 -0
  54. pygpt_net/provider/api/x_ai/__init__.py +30 -6
  55. pygpt_net/provider/api/x_ai/audio.py +43 -11
  56. pygpt_net/provider/api/x_ai/chat.py +92 -4
  57. pygpt_net/provider/api/x_ai/realtime/__init__.py +12 -0
  58. pygpt_net/provider/api/x_ai/realtime/client.py +1825 -0
  59. pygpt_net/provider/api/x_ai/realtime/realtime.py +198 -0
  60. pygpt_net/provider/api/x_ai/remote_tools.py +19 -1
  61. pygpt_net/provider/api/x_ai/store.py +610 -0
  62. pygpt_net/provider/api/x_ai/stream.py +30 -9
  63. pygpt_net/provider/api/x_ai/worker/importer.py +308 -0
  64. pygpt_net/provider/audio_input/xai_grok_voice.py +390 -0
  65. pygpt_net/provider/audio_output/xai_tts.py +325 -0
  66. pygpt_net/provider/core/config/patch.py +18 -3
  67. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +2 -2
  68. pygpt_net/provider/core/model/patch.py +13 -0
  69. pygpt_net/tools/image_viewer/tool.py +334 -34
  70. pygpt_net/tools/image_viewer/ui/dialogs.py +317 -21
  71. pygpt_net/ui/dialog/assistant.py +1 -1
  72. pygpt_net/ui/dialog/plugins.py +13 -5
  73. pygpt_net/ui/dialog/remote_store.py +552 -0
  74. pygpt_net/ui/dialogs.py +3 -5
  75. pygpt_net/ui/layout/ctx/ctx_list.py +58 -7
  76. pygpt_net/ui/menu/tools.py +6 -13
  77. pygpt_net/ui/widget/dialog/{remote_store_google.py → remote_store.py} +10 -10
  78. pygpt_net/ui/widget/element/button.py +4 -4
  79. pygpt_net/ui/widget/image/display.py +2 -2
  80. pygpt_net/ui/widget/lists/context.py +2 -2
  81. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/METADATA +9 -2
  82. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/RECORD +82 -70
  83. pygpt_net/controller/remote_store/google/store.py +0 -615
  84. pygpt_net/controller/remote_store/openai/batch.py +0 -524
  85. pygpt_net/controller/remote_store/openai/store.py +0 -699
  86. pygpt_net/ui/dialog/remote_store_google.py +0 -539
  87. pygpt_net/ui/dialog/remote_store_openai.py +0 -539
  88. pygpt_net/ui/widget/dialog/remote_store_openai.py +0 -56
  89. pygpt_net/ui/widget/lists/remote_store_google.py +0 -248
  90. pygpt_net/ui/widget/lists/remote_store_openai.py +0 -317
  91. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/LICENSE +0 -0
  92. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/WHEEL +0 -0
  93. {pygpt_net-2.7.7.dist-info → pygpt_net-2.7.8.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,325 @@
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: 2026.01.06 20:00:00 #
10
+ # ================================================== #
11
+
12
+ import asyncio
13
+ import base64
14
+ import json
15
+ import os
16
+ import queue
17
+ import threading
18
+ import wave
19
+ from typing import Optional, Tuple
20
+
21
+ from .base import BaseProvider
22
+
23
+
24
+ class XAITextToSpeech(BaseProvider):
25
+ def __init__(self, *args, **kwargs):
26
+ """
27
+ xAI Grok Voice Agent text-to-speech provider (via WebSocket).
28
+
29
+ :param args: args
30
+ :param kwargs: kwargs
31
+ """
32
+ super(XAITextToSpeech, self).__init__(*args, **kwargs)
33
+ self.plugin = kwargs.get("plugin")
34
+ self.id = "xai_tts"
35
+ self.name = "xAI TTS"
36
+
37
+ self.supported_voices = ["Ara", "Rex", "Sal", "Eve", "Leo"]
38
+
39
+ def init_options(self):
40
+ """Initialize options"""
41
+ self.plugin.add_option(
42
+ "xai_tts_voice",
43
+ type="text",
44
+ value="Ara",
45
+ label="Voice",
46
+ tab="xai_tts",
47
+ description="Specify xAI Grok Voice name (Ara, Rex, Sal, Eve, Leo)",
48
+ urls={"Voices": "https://docs.x.ai/docs/guides/voice/agent"},
49
+ )
50
+ self.plugin.add_option(
51
+ "xai_tts_sample_rate",
52
+ type="text",
53
+ value="24000",
54
+ label="Sample rate (Hz)",
55
+ tab="xai_tts",
56
+ description="PCM sample rate for output audio, e.g., 16000 or 24000",
57
+ )
58
+ self.plugin.add_option(
59
+ "xai_tts_instructions",
60
+ type="textarea",
61
+ value="You are a neutral TTS voice. Speak clearly and read the text verbatim.",
62
+ label="System Prompt",
63
+ tab="xai_tts",
64
+ description="System prompt to guide TTS style",
65
+ tooltip="System prompt for voice output",
66
+ persist=True,
67
+ )
68
+ self.plugin.add_option(
69
+ "xai_tts_file_container",
70
+ type="text",
71
+ value="wav",
72
+ label="File container",
73
+ tab="xai_tts",
74
+ description="wav or raw",
75
+ )
76
+ self.plugin.add_option(
77
+ "xai_tts_region",
78
+ type="text",
79
+ value="",
80
+ label="Region (optional)",
81
+ tab="xai_tts",
82
+ description="Regional endpoint like us-east-1; leave empty for global",
83
+ )
84
+
85
+ def speech(self, text: str) -> str:
86
+ """
87
+ Speech text to audio
88
+
89
+ :param text: text to speech
90
+ :return: path to generated audio file
91
+ """
92
+ try:
93
+ _ = self.plugin.window.core.api.xai.get_client()
94
+ except Exception:
95
+ pass
96
+
97
+ api_key = self._get_api_key()
98
+ if not api_key:
99
+ raise RuntimeError("xAI API key is not set. Please configure it in settings.")
100
+
101
+ voice = (self.plugin.get_option_value("xai_tts_voice") or "Ara").strip() or "Ara"
102
+ sr_opt = str(self.plugin.get_option_value("xai_tts_sample_rate") or "24000").strip()
103
+ try:
104
+ sample_rate = max(8000, int(sr_opt))
105
+ except Exception:
106
+ sample_rate = 24000
107
+ instructions = self.plugin.get_option_value("xai_tts_instructions") or ""
108
+ container = (self.plugin.get_option_value("xai_tts_file_container") or "wav").strip().lower()
109
+ if container not in ("wav", "raw"):
110
+ container = "wav"
111
+
112
+ region = (self.plugin.get_option_value("xai_tts_region") or "").strip()
113
+ host = f"{region}.api.x.ai" if region else "api.x.ai"
114
+ ws_uri = f"wss://{host}/v1/realtime"
115
+
116
+ base_dir = self.plugin.window.core.config.path
117
+ default_name = getattr(self.plugin, "output_file", "output.wav")
118
+ out_path = os.path.join(base_dir, default_name)
119
+ out_path = self._ensure_extension(out_path, ".wav" if container == "wav" else ".raw")
120
+
121
+ result_queue: queue.Queue[Tuple[bool, Optional[str], Optional[bytes]]] = queue.Queue()
122
+
123
+ def _runner():
124
+ loop = asyncio.new_event_loop()
125
+ try:
126
+ asyncio.set_event_loop(loop)
127
+ ok, err, pcm = loop.run_until_complete(
128
+ self._synthesize_async(
129
+ ws_uri=ws_uri,
130
+ api_key=api_key,
131
+ input_text=text,
132
+ voice=voice,
133
+ sample_rate=sample_rate,
134
+ instructions=instructions,
135
+ )
136
+ )
137
+ result_queue.put((ok, err, pcm))
138
+ finally:
139
+ try:
140
+ loop.close()
141
+ except Exception:
142
+ pass
143
+
144
+ t = threading.Thread(target=_runner, daemon=True)
145
+ t.start()
146
+ t.join()
147
+
148
+ ok, err, pcm_bytes = result_queue.get() if not result_queue.empty() else (False, "Unknown error", None)
149
+ if not ok or not pcm_bytes:
150
+ raise RuntimeError(err or "xAI TTS failed.")
151
+
152
+ if container == "wav":
153
+ self._write_wav(out_path, sample_rate, pcm_bytes)
154
+ else:
155
+ with open(out_path, "wb") as f:
156
+ f.write(pcm_bytes)
157
+
158
+ return str(out_path)
159
+
160
+ async def _synthesize_async(
161
+ self,
162
+ ws_uri: str,
163
+ api_key: str,
164
+ input_text: str,
165
+ voice: str,
166
+ sample_rate: int,
167
+ instructions: str,
168
+ ) -> Tuple[bool, Optional[str], Optional[bytes]]:
169
+ """
170
+ Connects to xAI Voice Agent realtime WebSocket and requests audio for the given text.
171
+ Returns (ok, error_message, pcm_bytes).
172
+ """
173
+ try:
174
+ import websockets # type: ignore
175
+ from websockets.asyncio.client import ClientConnection # type: ignore
176
+ except Exception:
177
+ return False, (
178
+ "The 'websockets' package is required for xAI TTS. Please install it in your environment."
179
+ ), None
180
+
181
+ audio_buf = bytearray()
182
+ transcript_buf = []
183
+
184
+ try:
185
+ async with websockets.connect(
186
+ uri=ws_uri,
187
+ ssl=True,
188
+ open_timeout=30,
189
+ close_timeout=10,
190
+ additional_headers={"Authorization": f"Bearer {api_key}"},
191
+ max_size=None,
192
+ ) as ws: # type: ClientConnection
193
+ session_config = {
194
+ "type": "session.update",
195
+ "session": {
196
+ "instructions": instructions,
197
+ "voice": voice,
198
+ "turn_detection": {"type": None},
199
+ "audio": {
200
+ "input": {"format": {"type": "audio/pcm", "rate": sample_rate}},
201
+ "output": {"format": {"type": "audio/pcm", "rate": sample_rate}},
202
+ },
203
+ },
204
+ }
205
+ await ws.send(json.dumps(session_config))
206
+
207
+ await ws.send(
208
+ json.dumps(
209
+ {
210
+ "type": "conversation.item.create",
211
+ "item": {
212
+ "type": "message",
213
+ "role": "user",
214
+ "content": [{"type": "input_text", "text": input_text}],
215
+ },
216
+ }
217
+ )
218
+ )
219
+
220
+ await ws.send(
221
+ json.dumps(
222
+ {
223
+ "type": "response.create",
224
+ "response": {
225
+ "modalities": ["text", "audio"],
226
+ },
227
+ }
228
+ )
229
+ )
230
+
231
+ end_time = asyncio.get_event_loop().time() + 90.0
232
+ got_any_audio = False
233
+
234
+ while True:
235
+ remaining = end_time - asyncio.get_event_loop().time()
236
+ if remaining <= 0:
237
+ return False, "Timed out waiting for xAI audio output.", None
238
+ try:
239
+ msg = await asyncio.wait_for(ws.recv(), timeout=remaining)
240
+ except asyncio.TimeoutError:
241
+ return False, "Timed out waiting for xAI audio output.", None
242
+ except Exception as e:
243
+ return False, f"WebSocket error: {e}", None
244
+
245
+ try:
246
+ event = json.loads(msg)
247
+ except Exception:
248
+ continue
249
+
250
+ etype = event.get("type", "")
251
+
252
+ if etype == "response.output_audio.delta":
253
+ # xAI sends base64 audio in the 'delta' field
254
+ chunk_b64 = event.get("delta")
255
+ if chunk_b64:
256
+ try:
257
+ audio_buf.extend(base64.b64decode(chunk_b64))
258
+ got_any_audio = True
259
+ except Exception:
260
+ pass
261
+ elif etype == "response.output_audio_transcript.delta":
262
+ # Collect transcript (not used for file, helpful for debugging)
263
+ delta_txt = event.get("delta")
264
+ if delta_txt:
265
+ transcript_buf.append(delta_txt)
266
+ elif etype == "response.output_audio.done":
267
+ # Wait for response.done to ensure turn completion
268
+ continue
269
+ elif etype == "response.done":
270
+ break
271
+ elif etype == "response.error":
272
+ return False, event.get("message") or "xAI TTS error.", None
273
+
274
+ if not got_any_audio:
275
+ # Provide a more helpful error if we at least got transcript
276
+ if transcript_buf:
277
+ return False, "Empty audio from xAI TTS, but transcript was returned.", None
278
+ return False, "Empty audio from xAI TTS.", None
279
+
280
+ return True, None, bytes(audio_buf)
281
+ except Exception as e:
282
+ return False, f"WebSocket connection failed: {e}", None
283
+
284
+ def _write_wav(self, path: str, sample_rate: int, pcm_bytes: bytes):
285
+ """
286
+ Writes PCM16LE mono samples into a WAV container.
287
+ """
288
+ with wave.open(path, "wb") as wf:
289
+ wf.setnchannels(1)
290
+ wf.setsampwidth(2)
291
+ wf.setframerate(sample_rate)
292
+ wf.writeframes(pcm_bytes)
293
+
294
+ def _ensure_extension(self, path: str, desired_ext: str) -> str:
295
+ """
296
+ Replaces the file extension with desired_ext.
297
+ """
298
+ root, _ = os.path.splitext(path)
299
+ return root + desired_ext
300
+
301
+ def _get_api_key(self) -> Optional[str]:
302
+ """
303
+ Resolve xAI API key from the app's configuration or environment.
304
+ """
305
+ key = self.plugin.window.core.config.get("api_key_xai")
306
+ if key:
307
+ return key
308
+ return os.getenv("XAI_API_KEY")
309
+
310
+ def is_configured(self) -> bool:
311
+ """
312
+ Check if provider is configured
313
+
314
+ :return: True if configured, False otherwise
315
+ """
316
+ api_key = self._get_api_key()
317
+ return api_key is not None and api_key != ""
318
+
319
+ def get_config_message(self) -> str:
320
+ """
321
+ Return message to display when provider is not configured
322
+
323
+ :return: message
324
+ """
325
+ return "xAI API key is not set yet. Please configure it in settings."
@@ -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: 2026.01.05 20:00:00 #
9
+ # Updated Date: 2026.01.06 06:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
@@ -234,8 +234,8 @@ class Patch:
234
234
  data["remote_tools.google.file_search.args"] = ""
235
235
  if "remote_tools.google.maps" not in data:
236
236
  data["remote_tools.google.maps"] = False
237
- if "remote_store.openai.hide_threads" not in data:
238
- data["remote_store.openai.hide_threads"] = True
237
+ if "remote_store.hide_threads" not in data:
238
+ data["remote_store.hide_threads"] = True
239
239
  updated = True
240
240
 
241
241
  # < 2.7.7
@@ -259,6 +259,21 @@ class Patch:
259
259
  data[key] = cfg_get_base(key)
260
260
  updated = True
261
261
 
262
+ # < 2.7.8
263
+ if old < parse_version("2.7.8"):
264
+ print("Migrating config from < 2.7.8...")
265
+ to_add = [
266
+ "remote_store.hide_threads",
267
+ "remote_store.provider",
268
+ "api_key_management_xai",
269
+ "remote_tools.xai.collections",
270
+ "remote_tools.xai.collections.args",
271
+ ]
272
+ for key in to_add:
273
+ if key not in data:
274
+ data[key] = cfg_get_base(key)
275
+ updated = True
276
+
262
277
  # update file
263
278
  migrated = False
264
279
  if updated:
@@ -1404,8 +1404,8 @@ class Patch:
1404
1404
  # < 2.1.79
1405
1405
  if old < parse_version("2.1.79"):
1406
1406
  print("Migrating config from < 2.1.79...")
1407
- if 'remote_store.openai.hide_threads' not in data:
1408
- data["remote_store.openai.hide_threads"] = True
1407
+ if 'remote_store.hide_threads' not in data:
1408
+ data["remote_store.hide_threads"] = True
1409
1409
  updated = True
1410
1410
 
1411
1411
  # < 2.2.2
@@ -141,6 +141,19 @@ class Patch:
141
141
  m.input.append("image")
142
142
  updated = True
143
143
 
144
+ # < 2.7.8 <--- add missing image input
145
+ if old < parse_version("2.7.8"):
146
+ print("Migrating models from < 2.7.8...")
147
+ models_to_update = [
148
+ "grok-4"
149
+ ]
150
+ for model in models_to_update:
151
+ if model in data:
152
+ m = data[model]
153
+ if not m.is_image_input():
154
+ m.input.append("image")
155
+ updated = True
156
+
144
157
  # update file
145
158
  if updated:
146
159
  # fix empty/broken data