pygpt-net 2.6.31__py3-none-any.whl → 2.6.33__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 (61) hide show
  1. pygpt_net/CHANGELOG.txt +12 -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/attachment.py +2 -0
  9. pygpt_net/controller/chat/common.py +18 -83
  10. pygpt_net/controller/lang/custom.py +2 -2
  11. pygpt_net/controller/media/__init__.py +12 -0
  12. pygpt_net/controller/media/media.py +115 -0
  13. pygpt_net/controller/painter/common.py +10 -11
  14. pygpt_net/controller/painter/painter.py +4 -12
  15. pygpt_net/controller/realtime/realtime.py +27 -2
  16. pygpt_net/controller/ui/mode.py +16 -2
  17. pygpt_net/core/audio/backend/pyaudio/realtime.py +51 -14
  18. pygpt_net/core/audio/output.py +3 -2
  19. pygpt_net/core/camera/camera.py +369 -53
  20. pygpt_net/core/image/image.py +6 -5
  21. pygpt_net/core/realtime/worker.py +1 -5
  22. pygpt_net/core/render/web/body.py +24 -3
  23. pygpt_net/core/text/utils.py +54 -2
  24. pygpt_net/core/types/image.py +7 -1
  25. pygpt_net/core/video/__init__.py +12 -0
  26. pygpt_net/core/video/video.py +290 -0
  27. pygpt_net/data/config/config.json +240 -212
  28. pygpt_net/data/config/models.json +243 -172
  29. pygpt_net/data/config/settings.json +194 -6
  30. pygpt_net/data/css/web-blocks.css +6 -0
  31. pygpt_net/data/css/web-chatgpt.css +6 -0
  32. pygpt_net/data/css/web-chatgpt_wide.css +6 -0
  33. pygpt_net/data/locale/locale.de.ini +31 -2
  34. pygpt_net/data/locale/locale.en.ini +41 -7
  35. pygpt_net/data/locale/locale.es.ini +31 -2
  36. pygpt_net/data/locale/locale.fr.ini +31 -2
  37. pygpt_net/data/locale/locale.it.ini +31 -2
  38. pygpt_net/data/locale/locale.pl.ini +34 -2
  39. pygpt_net/data/locale/locale.uk.ini +31 -2
  40. pygpt_net/data/locale/locale.zh.ini +31 -2
  41. pygpt_net/data/locale/plugin.cmd_web.en.ini +8 -0
  42. pygpt_net/item/model.py +22 -1
  43. pygpt_net/provider/api/google/__init__.py +38 -2
  44. pygpt_net/provider/api/google/video.py +364 -0
  45. pygpt_net/provider/api/openai/realtime/realtime.py +1 -2
  46. pygpt_net/provider/core/config/patch.py +226 -178
  47. pygpt_net/provider/core/model/patch.py +17 -2
  48. pygpt_net/provider/web/duckduck_search.py +212 -0
  49. pygpt_net/ui/layout/toolbox/audio.py +55 -0
  50. pygpt_net/ui/layout/toolbox/footer.py +14 -58
  51. pygpt_net/ui/layout/toolbox/image.py +3 -14
  52. pygpt_net/ui/layout/toolbox/raw.py +52 -0
  53. pygpt_net/ui/layout/toolbox/split.py +48 -0
  54. pygpt_net/ui/layout/toolbox/toolbox.py +8 -8
  55. pygpt_net/ui/layout/toolbox/video.py +49 -0
  56. pygpt_net/ui/widget/draw/painter.py +452 -84
  57. {pygpt_net-2.6.31.dist-info → pygpt_net-2.6.33.dist-info}/METADATA +28 -11
  58. {pygpt_net-2.6.31.dist-info → pygpt_net-2.6.33.dist-info}/RECORD +61 -51
  59. {pygpt_net-2.6.31.dist-info → pygpt_net-2.6.33.dist-info}/LICENSE +0 -0
  60. {pygpt_net-2.6.31.dist-info → pygpt_net-2.6.33.dist-info}/WHEEL +0 -0
  61. {pygpt_net-2.6.31.dist-info → pygpt_net-2.6.33.dist-info}/entry_points.txt +0 -0
@@ -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.19 07:00:00 #
9
+ # Updated Date: 2025.09.01 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -15,6 +15,7 @@ from random import shuffle as _shuffle
15
15
 
16
16
  from typing import Optional, List, Dict
17
17
 
18
+ from pygpt_net.core.text.utils import elide_filename
18
19
  from pygpt_net.core.events import Event
19
20
  from pygpt_net.item.ctx import CtxItem
20
21
  from pygpt_net.utils import trans
@@ -25,6 +26,7 @@ import pygpt_net.js_rc
25
26
  import pygpt_net.css_rc
26
27
  import pygpt_net.fonts_rc
27
28
 
29
+
28
30
  class Body:
29
31
 
30
32
  NUM_TIPS = 13
@@ -1066,7 +1068,7 @@ class Body:
1066
1068
  num_all: Optional[int] = None
1067
1069
  ) -> str:
1068
1070
  """
1069
- Get image HTML
1071
+ Get media image/video/audio HTML
1070
1072
 
1071
1073
  :param url: URL to image
1072
1074
  :param num: number of image
@@ -1075,7 +1077,26 @@ class Body:
1075
1077
  """
1076
1078
  url, path = self.window.core.filesystem.extract_local_url(url)
1077
1079
  basename = os.path.basename(path)
1078
- return f'<div class="extra-src-img-box" title="{url}"><div class="img-outer"><div class="img-wrapper"><a href="{url}"><img src="{path}" class="image"></a></div><a href="{url}" class="title">{basename}</a></div></div><br/>'
1080
+
1081
+ # if video file then embed video player
1082
+ ext = os.path.splitext(basename)[1].lower()
1083
+ video_exts = (".mp4", ".webm", ".ogg", ".mov", ".avi", ".mkv")
1084
+ if ext in video_exts:
1085
+ # check if .webm file exists for better compatibility
1086
+ if ext != ".webm":
1087
+ webm_path = os.path.splitext(path)[0] + ".webm"
1088
+ if os.path.exists(webm_path):
1089
+ path = webm_path
1090
+ ext = ".webm"
1091
+ return f'''
1092
+ <div class="extra-src-video-box" title="{url}">
1093
+ <video class="video-player" controls>
1094
+ <source src="{path}" type="video/{ext[1:]}">
1095
+ </video>
1096
+ <p><a href="{url}" class="title">{elide_filename(basename)}</a></p>
1097
+ </div>
1098
+ '''
1099
+ return f'<div class="extra-src-img-box" title="{url}"><div class="img-outer"><div class="img-wrapper"><a href="{url}"><img src="{path}" class="image"></a></div><a href="{url}" class="title">{elide_filename(basename)}</a></div></div><br/>'
1079
1100
 
1080
1101
  def get_url_html(
1081
1102
  self,
@@ -9,7 +9,6 @@
9
9
  # Updated Date: 2025.08.15 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
-
13
12
  def output_html2text(html: str) -> str:
14
13
  """
15
14
  Convert output HTML to plain text
@@ -76,4 +75,57 @@ def has_unclosed_code_tag(text: str) -> bool:
76
75
  """
77
76
  if not text:
78
77
  return False
79
- return (text.count('```') % 2) != 0
78
+ return (text.count('```') % 2) != 0
79
+
80
+ def elide_filename(name_or_path: str, max_len: int = 45, ellipsis: str = "...", keep_dir: bool = False) -> str:
81
+ """
82
+ Elide a long filename by replacing the middle with an ellipsis, preserving the extension.
83
+
84
+ Args:
85
+ name_or_path: Filename or full path.
86
+ max_len: Maximum length of the resulting string (including extension and ellipsis).
87
+ ellipsis: Ellipsis text to insert (e.g., "...").
88
+ keep_dir: If True and a path is provided, keep the directory prefix and elide only the basename.
89
+ If False, operate on the basename only.
90
+
91
+ Returns:
92
+ Elided filename (or path if keep_dir=True).
93
+ """
94
+ import os
95
+
96
+ if max_len <= 0:
97
+ return name_or_path
98
+
99
+ dirpart, base = os.path.split(name_or_path) if keep_dir else ("", os.path.basename(name_or_path))
100
+ stem, ext = os.path.splitext(base)
101
+
102
+ # if already short enough
103
+ if len(base) <= max_len:
104
+ return os.path.join(dirpart, base) if keep_dir else base
105
+
106
+ # minimal sanity for very small max_len
107
+ min_needed = len(ext) + len(ellipsis) + 2 # at least 1 char head + 1 char tail
108
+ if max_len < min_needed:
109
+ # degrade gracefully: keep first char, ellipsis, last char, and as much ext as fits
110
+ head = stem[:1] if stem else ""
111
+ tail = stem[-1:] if len(stem) > 1 else ""
112
+ # if ext is too long, trim it (rare edge case)
113
+ ext_trim = ext[: max(0, max_len - len(head) - len(ellipsis) - len(tail))]
114
+ out = f"{head}{ellipsis}{tail}{ext_trim}"
115
+ return os.path.join(dirpart, out) if keep_dir else out
116
+
117
+ # compute available budget for visible stem parts
118
+ avail = max_len - len(ext) - len(ellipsis)
119
+ # split budget between head and tail (favor head slightly)
120
+ head_len = (avail + 1) // 2
121
+ tail_len = avail - head_len
122
+
123
+ # guardrails
124
+ head_len = max(1, head_len)
125
+ tail_len = max(1, tail_len)
126
+
127
+ # build elided name
128
+ head = stem[:head_len]
129
+ tail = stem[-tail_len:] if tail_len <= len(stem) else stem
130
+ out = f"{head}{ellipsis}{tail}{ext}"
131
+ return os.path.join(dirpart, out) if keep_dir else out
@@ -6,9 +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.07.13 01:00:00 #
9
+ # Updated Date: 2025.09.01 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
+ VIDEO_AVAILABLE_ASPECT_RATIOS = {
13
+ "16:9": "16:9",
14
+ "9:16": "9:16",
15
+ }
16
+
17
+
12
18
  IMAGE_AVAILABLE_RESOLUTIONS = {
13
19
  "gpt-image": {
14
20
  "auto": "auto",
@@ -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 .video import Video
@@ -0,0 +1,290 @@
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
+ import uuid
13
+ import os
14
+ import shutil
15
+ import subprocess
16
+ from typing import Optional, List, Dict
17
+ from time import strftime
18
+
19
+ from PySide6.QtCore import Slot, QObject
20
+
21
+ from pygpt_net.core.types import VIDEO_AVAILABLE_ASPECT_RATIOS
22
+ from pygpt_net.item.ctx import CtxItem
23
+ from pygpt_net.utils import trans
24
+
25
+
26
+ class Video(QObject):
27
+ def __init__(self, window=None):
28
+ """
29
+ Video generation core
30
+
31
+ :param window: Window instance
32
+ """
33
+ super().__init__()
34
+ self.window = window
35
+
36
+ def install(self):
37
+ """Install provider data, img dir, etc."""
38
+ img_dir = os.path.join(self.window.core.config.get_user_dir("video"))
39
+ if not os.path.exists(img_dir):
40
+ os.makedirs(img_dir, exist_ok=True)
41
+
42
+ @Slot(object, list, str)
43
+ def handle_finished(
44
+ self,
45
+ ctx: CtxItem,
46
+ paths: List[str],
47
+ prompt: str
48
+ ):
49
+ """
50
+ Handle finished image generation
51
+
52
+ :param ctx: CtxItem
53
+ :param paths: images paths list
54
+ :param prompt: prompt used for generate images
55
+ """
56
+ self.window.controller.chat.image.handle_response(ctx, paths, prompt)
57
+
58
+ @Slot(object, list, str)
59
+ def handle_finished_inline(
60
+ self,
61
+ ctx: CtxItem,
62
+ paths: List[str],
63
+ prompt: str
64
+ ):
65
+ """
66
+ Handle finished image generation
67
+
68
+ :param ctx: CtxItem
69
+ :param paths: images paths list
70
+ :param prompt: prompt used for generate images
71
+ """
72
+ self.window.controller.chat.image.handle_response_inline(
73
+ ctx,
74
+ paths,
75
+ prompt,
76
+ )
77
+
78
+ @Slot(object)
79
+ def handle_status(self, msg: str):
80
+ """
81
+ Handle thread status message
82
+
83
+ :param msg: status message
84
+ """
85
+ self.window.update_status(msg)
86
+
87
+ is_log = False
88
+ if self.window.core.config.has("log.dalle") \
89
+ and self.window.core.config.get("log.dalle"):
90
+ is_log = True
91
+ self.window.core.debug.info(msg, not is_log)
92
+ if is_log:
93
+ print(msg)
94
+
95
+ @Slot(object)
96
+ def handle_error(self, msg: any):
97
+ """
98
+ Handle thread error message
99
+
100
+ :param msg: error message
101
+ """
102
+ self.window.update_status(msg)
103
+ self.window.core.debug.log(msg)
104
+ self.window.ui.dialogs.alert(msg)
105
+
106
+ def save_video(self, path: str, video: bytes) -> bool:
107
+ """
108
+ Save video to file
109
+
110
+ :param path: path to save
111
+ :param video: image data
112
+ :return: True if success
113
+ """
114
+ try:
115
+ with open(path, 'wb') as file:
116
+ file.write(video)
117
+ try:
118
+ # try to make web compatible
119
+ self.make_web_compatible(path)
120
+ except Exception as e:
121
+ pass
122
+ return True
123
+ except Exception as e:
124
+ print(trans('img.status.save.error') + ": " + str(e))
125
+ return False
126
+
127
+ def make_web_compatible(
128
+ self,
129
+ src_path: str,
130
+ fps: int = 30,
131
+ crf_h264: int = 22,
132
+ crf_vp9: int = 30,
133
+ audio_bitrate: str = "128k",
134
+ make_mp4: bool = True,
135
+ make_webm: bool = True,
136
+ overwrite: bool = True,
137
+ ) -> Dict[str, Optional[str]]:
138
+ """
139
+ Create browser-friendly video variants (MP4 H.264/AAC yuv420p + WebM VP9/Opus yuv420p).
140
+
141
+ Returns:
142
+ dict: {"mp4": "/abs/path/file.web.mp4" or None, "webm": "/abs/path/file.webm" or None}
143
+
144
+ Notes:
145
+ - Requires ffmpeg in PATH.
146
+ - Ensures even dimensions, yuv420p, faststart for MP4, and Opus for WebM.
147
+ - Uses CRF for quality: lower = better (and larger). Tweak crf_h264 / crf_vp9 if needed.
148
+ """
149
+ if not os.path.isfile(src_path):
150
+ raise FileNotFoundError(f"Source file not found: {src_path}")
151
+
152
+ # Ensure ffmpeg is available
153
+ ffmpeg = shutil.which("ffmpeg")
154
+ if not ffmpeg:
155
+ raise RuntimeError("ffmpeg not found in PATH. Please install ffmpeg.")
156
+
157
+ root, _ = os.path.splitext(os.path.abspath(src_path))
158
+ out_mp4 = f"{root}.web.mp4"
159
+ out_webm = f"{root}.webm"
160
+
161
+ # Remove outputs if overwrite is requested
162
+ if overwrite:
163
+ for p in (out_mp4, out_webm):
164
+ try:
165
+ if os.path.exists(p):
166
+ os.remove(p)
167
+ except Exception:
168
+ pass
169
+
170
+ # Common video filter:
171
+ # - scale to even dimensions (required by many encoders)
172
+ # - format to yuv420p (8-bit), also set SAR=1
173
+ vf = "scale=trunc(iw/2)*2:trunc(ih/2)*2:flags=lanczos,format=yuv420p,setsar=1"
174
+
175
+ results = {"mp4": None, "webm": None}
176
+
177
+ def run_cmd(cmd, dst):
178
+ # Run ffmpeg and return dst on success, None on failure
179
+ try:
180
+ subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
181
+ return dst if os.path.exists(dst) else None
182
+ except subprocess.CalledProcessError as e:
183
+ # If needed, print(e.stdout.decode(errors="ignore"))
184
+ return None
185
+
186
+ if make_mp4:
187
+ # H.264 High@4.1, yuv420p, AAC; add faststart for web playback
188
+ mp4_cmd = [
189
+ ffmpeg, "-y",
190
+ "-i", src_path,
191
+ "-map", "0:v:0", "-map", "0:a:0?", # include audio if present
192
+ "-vf", vf,
193
+ "-r", str(fps),
194
+ "-c:v", "libx264",
195
+ "-pix_fmt", "yuv420p",
196
+ "-profile:v", "high", "-level", "4.1",
197
+ "-preset", "medium",
198
+ "-crf", str(crf_h264),
199
+ "-color_primaries", "bt709", "-colorspace", "bt709", "-color_trc", "bt709",
200
+ "-movflags", "+faststart",
201
+ "-c:a", "aac", "-b:a", audio_bitrate, "-ac", "2", "-ar", "48000",
202
+ "-sn",
203
+ out_mp4,
204
+ ]
205
+ results["mp4"] = run_cmd(mp4_cmd, out_mp4)
206
+
207
+ if make_webm:
208
+ # VP9 (CRF, constant quality), Opus audio
209
+ webm_cmd = [
210
+ ffmpeg, "-y",
211
+ "-i", src_path,
212
+ "-map", "0:v:0", "-map", "0:a:0?",
213
+ "-vf", vf,
214
+ "-r", str(fps),
215
+ "-c:v", "libvpx-vp9",
216
+ "-b:v", "0", # use CRF mode
217
+ "-crf", str(crf_vp9),
218
+ "-row-mt", "1",
219
+ "-pix_fmt", "yuv420p",
220
+ "-deadline", "good", # "good" for quality; "realtime" for speed
221
+ "-cpu-used", "2", # lower = slower/better; tweak for performance
222
+ "-c:a", "libopus", "-b:a", audio_bitrate, "-ac", "2", "-ar", "48000",
223
+ "-sn",
224
+ out_webm,
225
+ ]
226
+ results["webm"] = run_cmd(webm_cmd, out_webm)
227
+
228
+ return results
229
+
230
+ def make_safe_filename(self, name: str) -> str:
231
+ """
232
+ Make safe filename
233
+
234
+ :param name: filename to make safe
235
+ :return: safe filename
236
+ """
237
+ def safe_char(c):
238
+ if c.isalnum():
239
+ return c
240
+ else:
241
+ return "_"
242
+ return "".join(safe_char(c) for c in name).rstrip("_")[:30]
243
+
244
+ def gen_unique_path(self, ctx: CtxItem):
245
+ """
246
+ Generate unique image path based on context
247
+
248
+ :param ctx: CtxItem
249
+ :return: unique image path
250
+ """
251
+ img_id = uuid.uuid4()
252
+ dt_prefix = strftime("%Y%m%d_%H%M%S")
253
+ img_dir = self.window.core.config.get_user_dir("img")
254
+ filename = f"{dt_prefix}_{img_id}.png"
255
+ return os.path.join(img_dir, filename)
256
+
257
+ def _normalize_model_name(self, model: str) -> str:
258
+ """
259
+ Normalize model id (strip optional 'models/' prefix).
260
+
261
+ :param model: model id
262
+ """
263
+ try:
264
+ return model.split("/")[-1]
265
+ except Exception:
266
+ return model
267
+
268
+ def get_aspect_ratio_option(self) -> dict:
269
+ """
270
+ Get image resolution option for UI
271
+
272
+ :return: dict
273
+ """
274
+ return {
275
+ "type": "combo",
276
+ "slider": True,
277
+ "label": "video.aspect_ratio",
278
+ "value": "16:9",
279
+ "keys": self.get_available_aspect_ratio(),
280
+ }
281
+
282
+ def get_available_aspect_ratio(self, model: str = None) -> Dict[str, str]:
283
+ """
284
+ Get available image resolutions
285
+
286
+ :param model: model name
287
+ :return: dict of available resolutions
288
+ """
289
+ return VIDEO_AVAILABLE_ASPECT_RATIOS
290
+