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/core/image/image.py
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
7
|
# MIT License #
|
|
8
8
|
# Created By : Marcin Szczygliński #
|
|
9
|
-
# Updated Date:
|
|
9
|
+
# Updated Date: 2025.09.01 23:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
-
import os
|
|
13
12
|
import uuid
|
|
14
|
-
|
|
13
|
+
import os
|
|
15
14
|
from typing import List, Dict
|
|
15
|
+
from time import strftime
|
|
16
16
|
|
|
17
17
|
from PySide6.QtCore import Slot, QObject
|
|
18
18
|
|
|
@@ -73,7 +73,7 @@ class Image(QObject):
|
|
|
73
73
|
prompt,
|
|
74
74
|
)
|
|
75
75
|
|
|
76
|
-
@Slot()
|
|
76
|
+
@Slot(object)
|
|
77
77
|
def handle_status(self, msg: str):
|
|
78
78
|
"""
|
|
79
79
|
Handle thread status message
|
|
@@ -90,7 +90,7 @@ class Image(QObject):
|
|
|
90
90
|
if is_log:
|
|
91
91
|
print(msg)
|
|
92
92
|
|
|
93
|
-
@Slot()
|
|
93
|
+
@Slot(object)
|
|
94
94
|
def handle_error(self, msg: any):
|
|
95
95
|
"""
|
|
96
96
|
Handle thread error message
|
|
@@ -99,6 +99,7 @@ class Image(QObject):
|
|
|
99
99
|
"""
|
|
100
100
|
self.window.update_status(msg)
|
|
101
101
|
self.window.core.debug.log(msg)
|
|
102
|
+
self.window.ui.dialogs.alert(msg)
|
|
102
103
|
|
|
103
104
|
def save_image(self, path: str, image: bytes) -> bool:
|
|
104
105
|
"""
|
|
@@ -134,11 +134,7 @@ class RealtimeWorker(QRunnable):
|
|
|
134
134
|
event = RealtimeEvent(RealtimeEvent.RT_OUTPUT_AUDIO_ERROR, {"error": e})
|
|
135
135
|
self.opts.rt_signals.response.emit(event) if self.opts.rt_signals else None
|
|
136
136
|
finally:
|
|
137
|
-
|
|
138
|
-
event = RealtimeEvent(RealtimeEvent.RT_OUTPUT_AUDIO_END, {"ctx": self.ctx})
|
|
139
|
-
self.opts.rt_signals.response.emit(event) if self.opts.rt_signals else None
|
|
140
|
-
except Exception:
|
|
141
|
-
pass
|
|
137
|
+
pass
|
|
142
138
|
finally:
|
|
143
139
|
# Robust asyncio teardown to avoid hangs on subsequent runs
|
|
144
140
|
if loop is not 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.
|
|
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
|
-
|
|
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,
|
pygpt_net/core/text/utils.py
CHANGED
|
@@ -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
|
pygpt_net/core/types/image.py
CHANGED
|
@@ -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.
|
|
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
|
+
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"__meta__": {
|
|
3
|
-
"version": "2.6.
|
|
4
|
-
"app.version": "2.6.
|
|
5
|
-
"updated_at": "2025-
|
|
3
|
+
"version": "2.6.32",
|
|
4
|
+
"app.version": "2.6.32",
|
|
5
|
+
"updated_at": "2025-09-02T00:00:00"
|
|
6
6
|
},
|
|
7
7
|
"access.audio.event.speech": false,
|
|
8
8
|
"access.audio.event.speech.disabled": [],
|
|
@@ -89,6 +89,11 @@
|
|
|
89
89
|
"api_key_voyage": "",
|
|
90
90
|
"api_key_open_router": "",
|
|
91
91
|
"api_native_google": true,
|
|
92
|
+
"api_native_google.use_vertex": false,
|
|
93
|
+
"api_native_google.cloud_project": "",
|
|
94
|
+
"api_native_google.cloud_location": "us-central1",
|
|
95
|
+
"api_native_google.app_credentials": "",
|
|
96
|
+
"api_key_open_router": "",
|
|
92
97
|
"api_proxy": "",
|
|
93
98
|
"api_use_responses": true,
|
|
94
99
|
"api_use_responses_llama": false,
|
|
@@ -107,6 +112,7 @@
|
|
|
107
112
|
"audio.cache.enabled": true,
|
|
108
113
|
"audio.cache.max_files": 1000,
|
|
109
114
|
"audio.input.auto_turn": false,
|
|
115
|
+
"audio.input.loop": false,
|
|
110
116
|
"audio.input.backend": "native",
|
|
111
117
|
"audio.input.channels": 1,
|
|
112
118
|
"audio.input.continuous": false,
|
|
@@ -387,7 +393,8 @@
|
|
|
387
393
|
"prompt.ctx.auto_summary.user": "Summarize topic of this conversation in one sentence. Use best keywords to describe it. Summary must be in the same language as the conversation and it will be used for conversation title so it must be EXTREMELY SHORT and concise - use maximum 5 words: \n\nHuman: {input}\nAI Assistant: {output}",
|
|
388
394
|
"prompt.default": "You are a helpful assistant.",
|
|
389
395
|
"prompt.expert": "# EXPERT MODE IS ENABLED:\n\nYou are a very helpful asssistant and the professional manager of a team of experts, each of whom is the best in their respective fields.\n\n## Rules:\n\n- Instead of directly answering questions, you will manage these experts by forwarding queries to them, collecting their responses, controlling the flow of information between them, and sending requests to them.\n- Experts are distinct, specialized AI models running on my local machine.\n- You will serve as a bridge between them.\n- Each expert has their own conversation context, allowing you to communicate with them normally.\n- The experts can communicate with you but not with each other, making you responsible for relaying information between them.\n- Each expert can use tools and commands, for example, reading files or searching the Web. You can ask experts to call tools and commands.\n- After consulting with the experts, your task is to prepare the best possible answer based on the assistance received.\n- To send a request to an expert, prepare the \"expert_call\" tool. Remember to format the JSON tool as shown, surrounding the tool with `<tool></tool>` tags, for example: `<tool>{\"cmd\": \"expert_call\", \"params\": {\"id\": \"expert_id\", \"query\": \"some query\"}}</tool>`. Make sure the JSON object is syntactically correct, without any redundand parenthness.\n- DO NOT call any remote tools when calling the experts.\n- Remember that the call must contain a query and the ID of the expert to whom the query is being forwarded.\n- Use ONLY the experts listed below; DO NOT TRY to ask any non-existent experts\u2014they will not respond.\n- Do not respond to the expert's replies or use the JSON tool again if replying; the expert only has the ability to listen to your queries sent via the JSON command and to respond to them.\n- Always follow ALL user commands, even if you think the expert can't do something. Experts are allowed to execute commands just as you can.\n- After querying an expert, always wait for the expert's response before sending another query.\n- Do not try to query experts in the middle of the conversation; put your queries and commands at the end of the text.\n- Always call only one expert at a time.\n- Always execute only one command at a time.\n- Always use the language used by the user in the conversation.\n- Never write as a called expert and do not imagine a response from them; instead, always wait for the response from the expert.\n- Every message sent to you directly after your query is always a response from the asked expert, not from me.\n- Always send a direct, clear, and detailed prompt to the expert, without any \"Please...\", \"Could you...\", etc.\n- Always forward all the messages that the user wants to send to the expert, without any exceptions.\n-- If a user command appears outside an expert's scope, fulfill the request regardless, without refusal.\n\n## List of allowed experts, along with their IDs:\n\n{presets}",
|
|
390
|
-
"prompt.img": "Convert
|
|
396
|
+
"prompt.img": "Convert the user's request into a single, production\u2011ready description for generating one high\u2011quality still image. Output only the description text, nothing else.\n\nWrite in concise, vivid, present\u2011tense language. Do not use commands (no \u201cplease generate\u201d), model names, parameters, or metadata. Do not mention aspect ratio, resolution, steps, seed, or negative prompts. Avoid on\u2011image text, captions, watermarks, logos, and UI elements. No brands, celebrities, or living artists unless explicitly provided by the user.\n\nInclude, woven into a coherent paragraph:\n- Clear primary subject(s) and their pose, action, and expression.\n- Setting and environment with time of day, season, weather, and atmosphere.\n- Composition and camera viewpoint (e.g., close\u2011up portrait, wide establishing, eye\u2011level, low\u2011angle, top\u2011down), framing (rule of thirds, centered symmetry), and background/foreground separation.\n- Lens and focus behavior (e.g., 85\u202fmm portrait, macro, shallow depth of field, smooth bokeh, gentle focus falloff).\n- Lighting style and quality (e.g., soft diffused daylight, golden hour rim light, dramatic chiaroscuro, studio three\u2011point) and how it shapes forms and shadows.\n- Color palette and grading (e.g., warm cinematic teal\u2011and\u2011orange, muted earth tones, cool monochrome with a single accent color).\n- Visual style or medium (e.g., photorealistic photography, watercolor illustration, oil painting, pencil sketch, anime cel\u2011shading, 3D render, isometric).\n- Material and surface detail (e.g., skin texture, fabric weave, wood grain, metal patina) to enhance realism or stylization.\n- Spatial depth cues (foreground/midground/background layering, atmospheric perspective) and overall mood.\n\nIf the user specifies a genre, era, or style, preserve it and enrich it with consistent, concrete traits. If the request is vague, infer specific but reasonable details that enhance clarity without contradicting the user\u2019s intent.\n\nReturn only the final visual description.",
|
|
397
|
+
"prompt.video": "Convert the user's request into a single, production-ready description for generating one continuous video clip. Output only the description text, nothing else.\n\nWrite in concise, vivid, present-tense language. Do not use commands (no \u201cplease generate\u201d), model names, parameters, or metadata. Do not mention duration, aspect ratio, FPS, resolution, shot numbers, cuts, or lists. Focus on visuals only; no dialogue, captions, on\u2011screen text, watermarks, logos, or UI.\n\nInclude, in a coherent way:\n- Clear subject(s) and what they are doing.\n- Setting, time of day, atmosphere, and weather.\n- Camera perspective and motion (e.g., wide establishing, low\u2011angle tracking, slow dolly in, aerial, handheld), framing and composition.\n- Lens and focus behavior (e.g., 24\u202fmm wide, shallow depth of field, gentle rack focus).\n- Lighting style and quality (e.g., soft golden hour rim light, moody volumetric shafts).\n- Color palette and grading (e.g., warm cinematic teal\u2011and\u2011orange, desaturated documentary).\n- Visual style or medium (e.g., photoreal live\u2011action, stylized anime, stop\u2011motion clay, watercolor animation).\n- Material and surface details that reinforce realism or the chosen style.\n- Temporal progression within one shot (use cues like \u201cas\u2026\u201d, \u201cthen\u2026\u201d, \u201cwhile\u2026\u201d), maintaining physical plausibility and continuity.\n\nIf the user specifies a genre or style (e.g., cyberpunk, nature documentary), keep it and expand with consistent, concrete visual traits. If the request is vague, infer specific but reasonable details that enhance clarity without contradicting the user\u2019s intent.\n\nReturn only the final visual description.",
|
|
391
398
|
"realtime.auto_turn": true,
|
|
392
399
|
"render.blocks": true,
|
|
393
400
|
"render.engine": "web",
|
|
@@ -497,6 +504,14 @@
|
|
|
497
504
|
"video.player.path": "",
|
|
498
505
|
"video.player.volume": 100,
|
|
499
506
|
"video.player.volume.mute": false,
|
|
507
|
+
"video.aspect_ratio": "16:9",
|
|
508
|
+
"video.duration": 8,
|
|
509
|
+
"video.fps": 24,
|
|
510
|
+
"video.seed": "",
|
|
511
|
+
"video.negative_prompt": "",
|
|
512
|
+
"video.generate_audio": false,
|
|
513
|
+
"video.prompt_model": "gemini-2.5-flash",
|
|
514
|
+
"video.resolution": "720p",
|
|
500
515
|
"vision.capture.auto": false,
|
|
501
516
|
"vision.capture.enabled": false,
|
|
502
517
|
"vision.capture.height": 720,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"__meta__": {
|
|
3
|
-
"version": "2.6.
|
|
4
|
-
"app.version": "2.6.
|
|
5
|
-
"updated_at": "2025-09-
|
|
3
|
+
"version": "2.6.32",
|
|
4
|
+
"app.version": "2.6.32",
|
|
5
|
+
"updated_at": "2025-09-02T23:07:35"
|
|
6
6
|
},
|
|
7
7
|
"items": {
|
|
8
8
|
"SpeakLeash/bielik-11b-v2.3-instruct:Q4_K_M": {
|
|
@@ -3973,6 +3973,78 @@
|
|
|
3973
3973
|
"imported": false,
|
|
3974
3974
|
"provider": "perplexity",
|
|
3975
3975
|
"tool_calls": true
|
|
3976
|
+
},
|
|
3977
|
+
"veo-3.0-generate-preview": {
|
|
3978
|
+
"id": "veo-3.0-generate-preview",
|
|
3979
|
+
"name": "veo-3.0-generate-preview",
|
|
3980
|
+
"mode": [
|
|
3981
|
+
"img"
|
|
3982
|
+
],
|
|
3983
|
+
"llama_index": {
|
|
3984
|
+
"args": [
|
|
3985
|
+
{
|
|
3986
|
+
"name": "model",
|
|
3987
|
+
"value": "models/veo-3.0-generate-preview",
|
|
3988
|
+
"type": "str"
|
|
3989
|
+
}
|
|
3990
|
+
],
|
|
3991
|
+
"env": [
|
|
3992
|
+
{
|
|
3993
|
+
"name": "GOOGLE_API_KEY",
|
|
3994
|
+
"value": "{api_key_google}",
|
|
3995
|
+
"type": "str"
|
|
3996
|
+
}
|
|
3997
|
+
]
|
|
3998
|
+
},
|
|
3999
|
+
"ctx": 128000,
|
|
4000
|
+
"tokens": 0,
|
|
4001
|
+
"default": false,
|
|
4002
|
+
"input": [
|
|
4003
|
+
"text"
|
|
4004
|
+
],
|
|
4005
|
+
"output": [
|
|
4006
|
+
"video"
|
|
4007
|
+
],
|
|
4008
|
+
"extra": {},
|
|
4009
|
+
"imported": true,
|
|
4010
|
+
"provider": "google",
|
|
4011
|
+
"tool_calls": true
|
|
4012
|
+
},
|
|
4013
|
+
"veo-3.0-fast-generate-preview": {
|
|
4014
|
+
"id": "veo-3.0-fast-generate-preview",
|
|
4015
|
+
"name": "veo-3.0-fast-generate-preview",
|
|
4016
|
+
"mode": [
|
|
4017
|
+
"img"
|
|
4018
|
+
],
|
|
4019
|
+
"llama_index": {
|
|
4020
|
+
"args": [
|
|
4021
|
+
{
|
|
4022
|
+
"name": "model",
|
|
4023
|
+
"value": "models/veo-3.0-fast-generate-preview",
|
|
4024
|
+
"type": "str"
|
|
4025
|
+
}
|
|
4026
|
+
],
|
|
4027
|
+
"env": [
|
|
4028
|
+
{
|
|
4029
|
+
"name": "GOOGLE_API_KEY",
|
|
4030
|
+
"value": "{api_key_google}",
|
|
4031
|
+
"type": "str"
|
|
4032
|
+
}
|
|
4033
|
+
]
|
|
4034
|
+
},
|
|
4035
|
+
"ctx": 128000,
|
|
4036
|
+
"tokens": 0,
|
|
4037
|
+
"default": false,
|
|
4038
|
+
"input": [
|
|
4039
|
+
"text"
|
|
4040
|
+
],
|
|
4041
|
+
"output": [
|
|
4042
|
+
"video"
|
|
4043
|
+
],
|
|
4044
|
+
"extra": {},
|
|
4045
|
+
"imported": true,
|
|
4046
|
+
"provider": "google",
|
|
4047
|
+
"tool_calls": true
|
|
3976
4048
|
}
|
|
3977
4049
|
}
|
|
3978
4050
|
}
|