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.
- pygpt_net/CHANGELOG.txt +12 -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/attachment.py +2 -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/painter/common.py +10 -11
- pygpt_net/controller/painter/painter.py +4 -12
- 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/camera/camera.py +369 -53
- 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 +240 -212
- pygpt_net/data/config/models.json +243 -172
- 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 +31 -2
- pygpt_net/data/locale/locale.en.ini +41 -7
- pygpt_net/data/locale/locale.es.ini +31 -2
- pygpt_net/data/locale/locale.fr.ini +31 -2
- pygpt_net/data/locale/locale.it.ini +31 -2
- pygpt_net/data/locale/locale.pl.ini +34 -2
- pygpt_net/data/locale/locale.uk.ini +31 -2
- pygpt_net/data/locale/locale.zh.ini +31 -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/ui/widget/draw/painter.py +452 -84
- {pygpt_net-2.6.31.dist-info → pygpt_net-2.6.33.dist-info}/METADATA +28 -11
- {pygpt_net-2.6.31.dist-info → pygpt_net-2.6.33.dist-info}/RECORD +61 -51
- {pygpt_net-2.6.31.dist-info → pygpt_net-2.6.33.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.31.dist-info → pygpt_net-2.6.33.dist-info}/WHEEL +0 -0
- {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.
|
|
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
|
+
|