pygpt-net 2.7.2__py3-none-any.whl → 2.7.4__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 +382 -350
- pygpt_net/controller/chat/attachment.py +5 -1
- pygpt_net/controller/chat/image.py +40 -5
- pygpt_net/controller/files/files.py +3 -1
- pygpt_net/controller/layout/layout.py +2 -2
- pygpt_net/controller/media/media.py +70 -1
- pygpt_net/controller/theme/nodes.py +2 -1
- pygpt_net/controller/ui/mode.py +5 -1
- pygpt_net/controller/ui/ui.py +17 -2
- pygpt_net/core/filesystem/url.py +4 -1
- pygpt_net/core/render/web/helpers.py +5 -0
- pygpt_net/data/config/config.json +5 -4
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +0 -14
- pygpt_net/data/css/web-blocks.css +3 -0
- pygpt_net/data/css/web-chatgpt.css +3 -0
- pygpt_net/data/locale/locale.de.ini +6 -0
- pygpt_net/data/locale/locale.en.ini +7 -1
- pygpt_net/data/locale/locale.es.ini +6 -0
- pygpt_net/data/locale/locale.fr.ini +6 -0
- pygpt_net/data/locale/locale.it.ini +6 -0
- pygpt_net/data/locale/locale.pl.ini +7 -1
- pygpt_net/data/locale/locale.uk.ini +6 -0
- pygpt_net/data/locale/locale.zh.ini +6 -0
- pygpt_net/launcher.py +115 -55
- pygpt_net/preload.py +243 -0
- pygpt_net/provider/api/google/image.py +317 -10
- pygpt_net/provider/api/google/video.py +160 -4
- pygpt_net/provider/api/openai/image.py +201 -93
- pygpt_net/provider/api/openai/video.py +99 -24
- pygpt_net/provider/api/x_ai/image.py +25 -2
- pygpt_net/provider/core/config/patch.py +17 -1
- pygpt_net/ui/layout/chat/input.py +20 -2
- pygpt_net/ui/layout/chat/painter.py +6 -4
- pygpt_net/ui/layout/toolbox/image.py +21 -11
- pygpt_net/ui/layout/toolbox/raw.py +2 -2
- pygpt_net/ui/layout/toolbox/video.py +22 -9
- pygpt_net/ui/main.py +84 -3
- pygpt_net/ui/widget/dialog/base.py +3 -10
- pygpt_net/ui/widget/option/combo.py +119 -1
- pygpt_net/ui/widget/textarea/input_extra.py +664 -0
- {pygpt_net-2.7.2.dist-info → pygpt_net-2.7.4.dist-info}/METADATA +27 -20
- {pygpt_net-2.7.2.dist-info → pygpt_net-2.7.4.dist-info}/RECORD +48 -46
- {pygpt_net-2.7.2.dist-info → pygpt_net-2.7.4.dist-info}/LICENSE +0 -0
- {pygpt_net-2.7.2.dist-info → pygpt_net-2.7.4.dist-info}/WHEEL +0 -0
- {pygpt_net-2.7.2.dist-info → pygpt_net-2.7.4.dist-info}/entry_points.txt +0 -0
|
@@ -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: 2025.
|
|
9
|
+
# Updated Date: 2025.12.31 16:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import base64
|
|
13
13
|
import datetime
|
|
14
14
|
import os
|
|
15
|
-
from typing import Optional, Dict, Any
|
|
15
|
+
from typing import Optional, Dict, Any, List
|
|
16
16
|
|
|
17
17
|
import requests
|
|
18
18
|
|
|
@@ -51,12 +51,15 @@ class Image:
|
|
|
51
51
|
:param extra: Extra arguments
|
|
52
52
|
:param sync: Synchronous mode
|
|
53
53
|
"""
|
|
54
|
+
extra = extra or {}
|
|
54
55
|
prompt = context.prompt
|
|
55
56
|
ctx = context.ctx
|
|
56
57
|
model = context.model
|
|
57
58
|
num = extra.get("num", 1)
|
|
58
59
|
inline = extra.get("inline", False)
|
|
59
60
|
sub_mode = self.MODE_GENERATE
|
|
61
|
+
image_id = extra.get("image_id") # previous image reference for remix
|
|
62
|
+
extra_prompt = extra.get("extra_prompt", "")
|
|
60
63
|
|
|
61
64
|
# if attachments then switch mode to EDIT
|
|
62
65
|
attachments = context.attachments
|
|
@@ -72,27 +75,29 @@ class Image:
|
|
|
72
75
|
prompt_model = self.window.core.models.get(tmp_model)
|
|
73
76
|
|
|
74
77
|
# worker
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
78
|
+
worker = ImageWorker()
|
|
79
|
+
worker.window = self.window
|
|
80
|
+
worker.client = self.window.core.api.openai.get_client()
|
|
81
|
+
worker.ctx = ctx
|
|
82
|
+
worker.mode = sub_mode # mode can be "generate" or "edit"
|
|
83
|
+
worker.attachments = attachments # attachments for edit mode
|
|
84
|
+
worker.raw = self.window.core.config.get('img_raw')
|
|
85
|
+
worker.model = model.id # model ID for generate image, e.g. "dall-e-3"
|
|
86
|
+
worker.model_prompt = prompt_model # model for generate prompt, not image!
|
|
87
|
+
worker.input_prompt = prompt
|
|
88
|
+
worker.system_prompt = self.window.core.prompt.get('img')
|
|
89
|
+
worker.num = num
|
|
90
|
+
worker.inline = inline
|
|
91
|
+
worker.extra_prompt = extra_prompt
|
|
92
|
+
worker.image_id = image_id # remix: previous image path/identifier
|
|
88
93
|
|
|
89
94
|
# config
|
|
90
95
|
if self.window.core.config.has('img_quality'):
|
|
91
|
-
|
|
96
|
+
worker.quality = self.window.core.config.get('img_quality')
|
|
92
97
|
if self.window.core.config.has('img_resolution'):
|
|
93
|
-
|
|
98
|
+
worker.resolution = self.window.core.config.get('img_resolution')
|
|
94
99
|
|
|
95
|
-
|
|
100
|
+
self.worker = worker
|
|
96
101
|
self.worker.signals.finished.connect(self.window.core.image.handle_finished)
|
|
97
102
|
self.worker.signals.finished_inline.connect(self.window.core.image.handle_finished_inline)
|
|
98
103
|
self.worker.signals.status.connect(self.window.core.image.handle_status)
|
|
@@ -131,22 +136,25 @@ class ImageWorker(QRunnable):
|
|
|
131
136
|
self.kwargs = kwargs
|
|
132
137
|
self.window = None
|
|
133
138
|
self.client = None
|
|
134
|
-
self.ctx = None
|
|
139
|
+
self.ctx: Optional[CtxItem] = None
|
|
135
140
|
self.raw = False
|
|
136
141
|
self.mode = Image.MODE_GENERATE # default mode is generate
|
|
137
142
|
self.model = "dall-e-3"
|
|
138
143
|
self.quality = "standard"
|
|
139
144
|
self.resolution = "1792x1024"
|
|
140
|
-
self.attachments = {} # attachments for edit mode
|
|
145
|
+
self.attachments: Dict[str, Any] = {} # attachments for edit mode
|
|
141
146
|
self.model_prompt = None
|
|
142
|
-
self.input_prompt = None
|
|
147
|
+
self.input_prompt: Optional[str] = None
|
|
143
148
|
self.system_prompt = None
|
|
144
149
|
self.inline = False
|
|
150
|
+
self.extra_prompt: Optional[str] = None
|
|
145
151
|
self.num = 1
|
|
152
|
+
self.image_id: Optional[str] = None # previous image reference for remix
|
|
153
|
+
|
|
154
|
+
# legacy maps kept for backwards compatibility (dall-e-2 / dall-e-3 exact ids)
|
|
146
155
|
self.allowed_max_num = {
|
|
147
156
|
"dall-e-2": 4,
|
|
148
157
|
"dall-e-3": 1,
|
|
149
|
-
"gpt-image-1": 1,
|
|
150
158
|
}
|
|
151
159
|
self.allowed_resolutions = {
|
|
152
160
|
"dall-e-2": [
|
|
@@ -159,29 +167,60 @@ class ImageWorker(QRunnable):
|
|
|
159
167
|
"1024x1792",
|
|
160
168
|
"1024x1024",
|
|
161
169
|
],
|
|
162
|
-
"gpt-image-1": [
|
|
163
|
-
"1536x1024",
|
|
164
|
-
"1024x1536",
|
|
165
|
-
"1024x1024",
|
|
166
|
-
"auto",
|
|
167
|
-
],
|
|
168
170
|
}
|
|
169
171
|
self.allowed_quality = {
|
|
170
|
-
"dall-e-2": [
|
|
171
|
-
|
|
172
|
-
],
|
|
173
|
-
"dall-e-3": [
|
|
174
|
-
"standard",
|
|
175
|
-
"hd",
|
|
176
|
-
],
|
|
177
|
-
"gpt-image-1": [
|
|
178
|
-
"auto",
|
|
179
|
-
"high",
|
|
180
|
-
"medium",
|
|
181
|
-
"low",
|
|
182
|
-
],
|
|
172
|
+
"dall-e-2": ["standard"],
|
|
173
|
+
"dall-e-3": ["standard", "hd"],
|
|
183
174
|
}
|
|
184
175
|
|
|
176
|
+
# ---------- model helpers ----------
|
|
177
|
+
|
|
178
|
+
def _is_gpt_image_model(self, model_id: Optional[str] = None) -> bool:
|
|
179
|
+
mid = (model_id or self.model or "").lower()
|
|
180
|
+
return mid.startswith("gpt-image-1")
|
|
181
|
+
|
|
182
|
+
def _is_dalle2(self, model_id: Optional[str] = None) -> bool:
|
|
183
|
+
mid = (model_id or self.model or "").lower()
|
|
184
|
+
return mid == "dall-e-2"
|
|
185
|
+
|
|
186
|
+
def _is_dalle3(self, model_id: Optional[str] = None) -> bool:
|
|
187
|
+
mid = (model_id or self.model or "").lower()
|
|
188
|
+
return mid == "dall-e-3"
|
|
189
|
+
|
|
190
|
+
def _max_num_for_model(self) -> int:
|
|
191
|
+
if self._is_gpt_image_model():
|
|
192
|
+
return 1
|
|
193
|
+
if self._is_dalle2():
|
|
194
|
+
return self.allowed_max_num["dall-e-2"]
|
|
195
|
+
if self._is_dalle3():
|
|
196
|
+
return self.allowed_max_num["dall-e-3"]
|
|
197
|
+
return 1
|
|
198
|
+
|
|
199
|
+
def _normalize_resolution_for_model(self, resolution: Optional[str]) -> str:
|
|
200
|
+
res = (resolution or "").strip() or "1024x1024"
|
|
201
|
+
if self._is_gpt_image_model():
|
|
202
|
+
allowed = {"1024x1024", "1536x1024", "1024x1536", "auto"}
|
|
203
|
+
return res if res in allowed else "auto"
|
|
204
|
+
if self._is_dalle2():
|
|
205
|
+
allowed = set(self.allowed_resolutions["dall-e-2"])
|
|
206
|
+
return res if res in allowed else "1024x1024"
|
|
207
|
+
if self._is_dalle3():
|
|
208
|
+
allowed = set(self.allowed_resolutions["dall-e-3"])
|
|
209
|
+
return res if res in allowed else "1024x1024"
|
|
210
|
+
return res
|
|
211
|
+
|
|
212
|
+
def _normalize_quality_for_model(self, quality: Optional[str]) -> Optional[str]:
|
|
213
|
+
q = (quality or "").strip().lower()
|
|
214
|
+
if self._is_gpt_image_model():
|
|
215
|
+
allowed = {"auto", "high", "medium", "low"}
|
|
216
|
+
return q if q in allowed else "auto"
|
|
217
|
+
if self._is_dalle2():
|
|
218
|
+
return "standard"
|
|
219
|
+
if self._is_dalle3():
|
|
220
|
+
allowed = {"standard", "hd"}
|
|
221
|
+
return q if q in allowed else "standard"
|
|
222
|
+
return None
|
|
223
|
+
|
|
185
224
|
@Slot()
|
|
186
225
|
def run(self):
|
|
187
226
|
"""Run worker"""
|
|
@@ -209,55 +248,102 @@ class ImageWorker(QRunnable):
|
|
|
209
248
|
self.signals.error.emit(e)
|
|
210
249
|
self.signals.status.emit(trans('img.status.prompt.error') + ": " + str(e))
|
|
211
250
|
|
|
251
|
+
# Fallback negative prompt injection (OpenAI Images API has no native negative_prompt field)
|
|
252
|
+
if self.extra_prompt and str(self.extra_prompt).strip():
|
|
253
|
+
try:
|
|
254
|
+
self.input_prompt = self._merge_negative_prompt(self.input_prompt or "", self.extra_prompt)
|
|
255
|
+
except Exception:
|
|
256
|
+
pass
|
|
257
|
+
|
|
212
258
|
self.signals.status.emit(trans('img.status.generating') + ": {}...".format(self.input_prompt))
|
|
213
259
|
|
|
214
|
-
paths = [] # downloaded images paths
|
|
260
|
+
paths: List[str] = [] # downloaded images paths
|
|
215
261
|
try:
|
|
216
|
-
#
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
# check if resolution is supported
|
|
222
|
-
resolution = self.resolution
|
|
223
|
-
if self.model in self.allowed_resolutions:
|
|
224
|
-
if resolution not in self.allowed_resolutions[self.model]:
|
|
225
|
-
resolution = self.allowed_resolutions[self.model][0]
|
|
226
|
-
|
|
227
|
-
quality = self.quality
|
|
228
|
-
if self.model in self.allowed_quality:
|
|
229
|
-
if quality not in self.allowed_quality[self.model]:
|
|
230
|
-
quality = self.allowed_quality[self.model][0]
|
|
231
|
-
|
|
232
|
-
# send to API
|
|
262
|
+
# enforce model-specific limits/options
|
|
263
|
+
self.num = min(max(1, int(self.num or 1)), self._max_num_for_model())
|
|
264
|
+
resolution = self._normalize_resolution_for_model(self.resolution)
|
|
265
|
+
quality = self._normalize_quality_for_model(self.quality)
|
|
266
|
+
|
|
233
267
|
response = None
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
268
|
+
|
|
269
|
+
# Remix path: if image_id provided, prefer editing with previous image reference
|
|
270
|
+
if self.image_id:
|
|
271
|
+
if self._is_dalle3():
|
|
272
|
+
try:
|
|
273
|
+
self.signals.status.emit("Remix is not supported for this model; generating a new image.")
|
|
274
|
+
except Exception:
|
|
275
|
+
pass
|
|
276
|
+
else:
|
|
277
|
+
remix_images = []
|
|
278
|
+
try:
|
|
279
|
+
if isinstance(self.image_id, str) and os.path.exists(self.image_id):
|
|
280
|
+
remix_images.append(open(self.image_id, "rb"))
|
|
281
|
+
except Exception:
|
|
282
|
+
remix_images = []
|
|
283
|
+
|
|
284
|
+
if len(remix_images) > 0:
|
|
285
|
+
try:
|
|
286
|
+
edit_kwargs = {
|
|
287
|
+
"model": self.model,
|
|
288
|
+
"image": remix_images,
|
|
289
|
+
"prompt": self.input_prompt,
|
|
290
|
+
"n": self.num,
|
|
291
|
+
"size": resolution,
|
|
292
|
+
}
|
|
293
|
+
if self._is_gpt_image_model() or self._is_dalle3():
|
|
294
|
+
if quality:
|
|
295
|
+
edit_kwargs["quality"] = quality
|
|
296
|
+
response = self.client.images.edit(**edit_kwargs)
|
|
297
|
+
finally:
|
|
298
|
+
for f in remix_images:
|
|
299
|
+
try:
|
|
300
|
+
f.close()
|
|
301
|
+
except Exception:
|
|
302
|
+
pass
|
|
303
|
+
|
|
304
|
+
# Normal API paths when remix not executed or unsupported
|
|
305
|
+
if response is None:
|
|
306
|
+
if self.mode == Image.MODE_GENERATE:
|
|
307
|
+
if self._is_dalle2():
|
|
308
|
+
response = self.client.images.generate(
|
|
309
|
+
model=self.model,
|
|
310
|
+
prompt=self.input_prompt,
|
|
311
|
+
n=self.num,
|
|
312
|
+
size=resolution,
|
|
313
|
+
)
|
|
314
|
+
else:
|
|
315
|
+
gen_kwargs = {
|
|
316
|
+
"model": self.model,
|
|
317
|
+
"prompt": self.input_prompt,
|
|
318
|
+
"n": self.num,
|
|
319
|
+
"size": resolution,
|
|
320
|
+
}
|
|
321
|
+
if (self._is_gpt_image_model() or self._is_dalle3()) and quality:
|
|
322
|
+
gen_kwargs["quality"] = quality
|
|
323
|
+
response = self.client.images.generate(**gen_kwargs)
|
|
324
|
+
elif self.mode == Image.MODE_EDIT:
|
|
325
|
+
images = []
|
|
326
|
+
for uuid in self.attachments or {}:
|
|
327
|
+
attachment = self.attachments[uuid]
|
|
328
|
+
if attachment.path and os.path.exists(attachment.path):
|
|
329
|
+
images.append(open(attachment.path, "rb"))
|
|
330
|
+
try:
|
|
331
|
+
edit_kwargs = {
|
|
332
|
+
"model": self.model,
|
|
333
|
+
"image": images,
|
|
334
|
+
"prompt": self.input_prompt,
|
|
335
|
+
"n": self.num,
|
|
336
|
+
"size": resolution,
|
|
337
|
+
}
|
|
338
|
+
if (self._is_gpt_image_model() or self._is_dalle3()) and quality:
|
|
339
|
+
edit_kwargs["quality"] = quality
|
|
340
|
+
response = self.client.images.edit(**edit_kwargs)
|
|
341
|
+
finally:
|
|
342
|
+
for f in images:
|
|
343
|
+
try:
|
|
344
|
+
f.close()
|
|
345
|
+
except Exception:
|
|
346
|
+
pass
|
|
261
347
|
|
|
262
348
|
# check response
|
|
263
349
|
if response is None:
|
|
@@ -278,14 +364,13 @@ class ImageWorker(QRunnable):
|
|
|
278
364
|
msg = trans('img.status.downloading') + " (" + str(i + 1) + " / " + str(self.num) + ") -> " + str(path)
|
|
279
365
|
self.signals.status.emit(msg)
|
|
280
366
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
res = requests.get(response.data[i].url)
|
|
367
|
+
item = response.data[i]
|
|
368
|
+
data = None
|
|
369
|
+
if getattr(item, "url", None):
|
|
370
|
+
res = requests.get(item.url)
|
|
286
371
|
data = res.content
|
|
287
|
-
|
|
288
|
-
data = base64.b64decode(
|
|
372
|
+
elif getattr(item, "b64_json", None):
|
|
373
|
+
data = base64.b64decode(item.b64_json)
|
|
289
374
|
|
|
290
375
|
# save image
|
|
291
376
|
if data and self.window.core.image.save_image(path, data):
|
|
@@ -293,6 +378,16 @@ class ImageWorker(QRunnable):
|
|
|
293
378
|
else:
|
|
294
379
|
self.signals.error.emit("Error saving image")
|
|
295
380
|
|
|
381
|
+
# store image_id for future remix (use first saved path as reference)
|
|
382
|
+
if paths:
|
|
383
|
+
try:
|
|
384
|
+
if not isinstance(self.ctx.extra, dict):
|
|
385
|
+
self.ctx.extra = {}
|
|
386
|
+
self.ctx.extra["image_id"] = paths[0]
|
|
387
|
+
self.window.core.ctx.update_item(self.ctx)
|
|
388
|
+
except Exception:
|
|
389
|
+
pass
|
|
390
|
+
|
|
296
391
|
# send finished signal
|
|
297
392
|
if self.inline:
|
|
298
393
|
self.signals.finished_inline.emit( # separated signal for inline mode
|
|
@@ -323,3 +418,16 @@ class ImageWorker(QRunnable):
|
|
|
323
418
|
sig.deleteLater()
|
|
324
419
|
except RuntimeError:
|
|
325
420
|
pass
|
|
421
|
+
|
|
422
|
+
# ---------- prompt utilities ----------
|
|
423
|
+
|
|
424
|
+
@staticmethod
|
|
425
|
+
def _merge_negative_prompt(prompt: str, negative: Optional[str]) -> str:
|
|
426
|
+
"""
|
|
427
|
+
Append a negative prompt to the main text prompt for providers without a native negative_prompt field.
|
|
428
|
+
"""
|
|
429
|
+
base = (prompt or "").strip()
|
|
430
|
+
neg = (negative or "").strip()
|
|
431
|
+
if not neg:
|
|
432
|
+
return base
|
|
433
|
+
return (base + ("\n" if base else "") + f"Negative prompt: {neg}").strip()
|
|
@@ -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.12.
|
|
9
|
+
# Updated Date: 2025.12.31 16:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import datetime
|
|
@@ -59,6 +59,8 @@ class Video:
|
|
|
59
59
|
prompt = context.prompt
|
|
60
60
|
num = int(extra.get("num", 1))
|
|
61
61
|
inline = bool(extra.get("inline", False))
|
|
62
|
+
video_id = extra.get("video_id")
|
|
63
|
+
extra_prompt = extra.get("extra_prompt", "")
|
|
62
64
|
|
|
63
65
|
# decide sub-mode based on attachments (image-to-video when image is attached)
|
|
64
66
|
sub_mode = self.MODE_GENERATE
|
|
@@ -85,6 +87,8 @@ class Video:
|
|
|
85
87
|
worker.raw = self.window.core.config.get('img_raw')
|
|
86
88
|
worker.num = num
|
|
87
89
|
worker.inline = inline
|
|
90
|
+
worker.extra_prompt = extra_prompt
|
|
91
|
+
worker.video_id = video_id
|
|
88
92
|
|
|
89
93
|
# optional params (app-level options)
|
|
90
94
|
worker.aspect_ratio = str(extra.get("aspect_ratio") or self.window.core.config.get('video.aspect_ratio') or "16:9")
|
|
@@ -155,6 +159,8 @@ class VideoWorker(QRunnable):
|
|
|
155
159
|
self.input_prompt = ""
|
|
156
160
|
self.system_prompt = ""
|
|
157
161
|
self.inline = False
|
|
162
|
+
self.extra_prompt: Optional[str] = None
|
|
163
|
+
self.video_id = None
|
|
158
164
|
self.raw = False
|
|
159
165
|
self.num = 1
|
|
160
166
|
|
|
@@ -169,6 +175,7 @@ class VideoWorker(QRunnable):
|
|
|
169
175
|
@Slot()
|
|
170
176
|
def run(self):
|
|
171
177
|
try:
|
|
178
|
+
kernel = self.window.controller.kernel
|
|
172
179
|
# Optional prompt enhancement via app default LLM
|
|
173
180
|
if not self.raw and not self.inline and self.input_prompt:
|
|
174
181
|
try:
|
|
@@ -189,6 +196,14 @@ class VideoWorker(QRunnable):
|
|
|
189
196
|
self.signals.error.emit(e)
|
|
190
197
|
self.signals.status.emit(trans('vid.status.prompt.error') + ": " + str(e))
|
|
191
198
|
|
|
199
|
+
# Negative prompt fallback: inject constraints into the text prompt (Sora has no native negative_prompt field)
|
|
200
|
+
if self.extra_prompt and str(self.extra_prompt).strip():
|
|
201
|
+
try:
|
|
202
|
+
self.input_prompt = self._merge_negative_prompt(self.input_prompt, self.extra_prompt)
|
|
203
|
+
except Exception:
|
|
204
|
+
# do not fail generation if merge fails
|
|
205
|
+
pass
|
|
206
|
+
|
|
192
207
|
# Sora API accepts a single video per create call; honor app's num but cap to 1 per job
|
|
193
208
|
_ = max(1, min(self.num, self.max_per_job))
|
|
194
209
|
|
|
@@ -199,42 +214,88 @@ class VideoWorker(QRunnable):
|
|
|
199
214
|
# Image-to-video: first image attachment as input_reference, must match "size"
|
|
200
215
|
image_path = self._first_image_attachment(self.attachments) if self.mode == Video.MODE_IMAGE_TO_VIDEO else None
|
|
201
216
|
|
|
202
|
-
|
|
217
|
+
# If remix requested, ignore any image input_reference
|
|
218
|
+
is_remix = bool(self.video_id)
|
|
219
|
+
if is_remix:
|
|
220
|
+
image_path = None # enforce remix over image-to-video
|
|
221
|
+
|
|
222
|
+
label = trans('vid.status.generating')
|
|
223
|
+
if is_remix:
|
|
224
|
+
label += " (remix)"
|
|
225
|
+
self.signals.status.emit(label + f": {self.input_prompt}...")
|
|
203
226
|
|
|
204
227
|
# Create job
|
|
205
|
-
|
|
206
|
-
"model": self.model or "sora-2",
|
|
207
|
-
"prompt": self.input_prompt or "",
|
|
208
|
-
"seconds": str(seconds),
|
|
209
|
-
"size": size,
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
# Attach image as input_reference; auto-resize to match requested size if needed
|
|
228
|
+
job = None
|
|
213
229
|
file_handle = None
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
230
|
+
|
|
231
|
+
if is_remix:
|
|
232
|
+
# Primary path: dedicated remix endpoint, inherits the original's length and size
|
|
233
|
+
last_exc = None
|
|
234
|
+
try:
|
|
235
|
+
job = self.client.videos.remix(
|
|
236
|
+
video_id=str(self.video_id),
|
|
237
|
+
prompt=self.input_prompt or "",
|
|
238
|
+
)
|
|
239
|
+
except Exception as e1:
|
|
240
|
+
# Fallbacks for older SDKs: pass remix id via create()
|
|
241
|
+
last_exc = e1
|
|
221
242
|
try:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
243
|
+
job = self.client.videos.create(
|
|
244
|
+
model=self.model or "sora-2",
|
|
245
|
+
prompt=self.input_prompt or "",
|
|
246
|
+
remix_id=str(self.video_id),
|
|
247
|
+
)
|
|
248
|
+
except Exception as e2:
|
|
249
|
+
last_exc = e2
|
|
250
|
+
try:
|
|
251
|
+
job = self.client.videos.create(
|
|
252
|
+
model=self.model or "sora-2",
|
|
253
|
+
prompt=self.input_prompt or "",
|
|
254
|
+
remix_video_id=str(self.video_id),
|
|
255
|
+
)
|
|
256
|
+
except Exception as e3:
|
|
257
|
+
last_exc = e3
|
|
258
|
+
if job is None:
|
|
259
|
+
raise last_exc or RuntimeError("Unable to start remix job.")
|
|
260
|
+
else:
|
|
261
|
+
create_kwargs: Dict[str, Any] = {
|
|
262
|
+
"model": self.model or "sora-2",
|
|
263
|
+
"prompt": self.input_prompt or "",
|
|
264
|
+
"seconds": str(seconds),
|
|
265
|
+
"size": size,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
# Attach image as input_reference; auto-resize to match requested size if needed
|
|
269
|
+
if image_path:
|
|
270
|
+
prepared = self._prepare_input_reference(image_path, size)
|
|
271
|
+
if prepared is not None:
|
|
272
|
+
create_kwargs["input_reference"] = prepared
|
|
273
|
+
else:
|
|
274
|
+
try:
|
|
275
|
+
file_handle = open(image_path, "rb")
|
|
276
|
+
create_kwargs["input_reference"] = file_handle
|
|
277
|
+
except Exception as e:
|
|
278
|
+
self.signals.error.emit(e)
|
|
279
|
+
|
|
280
|
+
job = self.client.videos.create(**create_kwargs)
|
|
226
281
|
|
|
227
|
-
job = self.client.videos.create(**create_kwargs)
|
|
228
282
|
video_id = self._safe_get(job, "id")
|
|
229
283
|
if not video_id:
|
|
230
|
-
# include raw payload for debugging
|
|
231
284
|
raise RuntimeError("Video job ID missing in create response.")
|
|
232
285
|
|
|
233
286
|
# Poll until completed (or failed/canceled)
|
|
287
|
+
if not isinstance(self.ctx.extra, dict):
|
|
288
|
+
self.ctx.extra = {}
|
|
289
|
+
self.ctx.extra['video_id'] = video_id # store video_id in ctx extra
|
|
290
|
+
self.window.core.ctx.update_item(self.ctx)
|
|
234
291
|
last_progress = None
|
|
235
292
|
last_status = None
|
|
236
293
|
while True:
|
|
294
|
+
if kernel.stopped():
|
|
295
|
+
break
|
|
237
296
|
time.sleep(5)
|
|
297
|
+
if kernel.stopped():
|
|
298
|
+
break
|
|
238
299
|
job = self.client.videos.retrieve(video_id)
|
|
239
300
|
status = self._safe_get(job, "status") or ""
|
|
240
301
|
progress = self._safe_get(job, "progress")
|
|
@@ -559,4 +620,18 @@ class VideoWorker(QRunnable):
|
|
|
559
620
|
except Exception:
|
|
560
621
|
pass
|
|
561
622
|
|
|
562
|
-
return " ".join(parts).strip()
|
|
623
|
+
return " ".join(parts).strip()
|
|
624
|
+
|
|
625
|
+
# ---------- prompt utilities ----------
|
|
626
|
+
|
|
627
|
+
@staticmethod
|
|
628
|
+
def _merge_negative_prompt(prompt: str, negative: Optional[str]) -> str:
|
|
629
|
+
"""
|
|
630
|
+
Append a negative prompt to the main text prompt for providers without a native negative_prompt field.
|
|
631
|
+
"""
|
|
632
|
+
base = (prompt or "").strip()
|
|
633
|
+
neg = (negative or "").strip()
|
|
634
|
+
if not neg:
|
|
635
|
+
return base
|
|
636
|
+
# Keep the user's original prompt intact and add clear constraint instructions.
|
|
637
|
+
return (base + ("\n" if base else "") + f"Negative prompt: {neg}").strip()
|
|
@@ -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.12.31 16:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import base64
|
|
@@ -49,6 +49,7 @@ class Image:
|
|
|
49
49
|
prompt = context.prompt
|
|
50
50
|
num = int(extra.get("num", 1))
|
|
51
51
|
inline = bool(extra.get("inline", False))
|
|
52
|
+
extra_prompt = extra.get("extra_prompt", "")
|
|
52
53
|
|
|
53
54
|
# Optional prompt enhancement model (same as in your Google path)
|
|
54
55
|
prompt_model = self.window.core.models.from_defaults()
|
|
@@ -66,6 +67,7 @@ class Image:
|
|
|
66
67
|
worker.raw = self.window.core.config.get('img_raw')
|
|
67
68
|
worker.num = num
|
|
68
69
|
worker.inline = inline
|
|
70
|
+
worker.extra_prompt = extra_prompt
|
|
69
71
|
|
|
70
72
|
self.worker = worker
|
|
71
73
|
self.worker.signals.finished.connect(self.window.core.image.handle_finished)
|
|
@@ -102,6 +104,7 @@ class ImageWorker(QRunnable):
|
|
|
102
104
|
self.input_prompt = ""
|
|
103
105
|
self.system_prompt = ""
|
|
104
106
|
self.inline = False
|
|
107
|
+
self.extra_prompt: Optional[str] = None
|
|
105
108
|
self.raw = False
|
|
106
109
|
self.num = 1
|
|
107
110
|
|
|
@@ -131,6 +134,13 @@ class ImageWorker(QRunnable):
|
|
|
131
134
|
self.signals.error.emit(e)
|
|
132
135
|
self.signals.status.emit(trans('img.status.prompt.error') + ": " + str(e))
|
|
133
136
|
|
|
137
|
+
# Negative prompt fallback: append as textual instruction (xAI has no native field for it)
|
|
138
|
+
if self.extra_prompt and str(self.extra_prompt).strip():
|
|
139
|
+
try:
|
|
140
|
+
self.input_prompt = self._merge_negative_prompt(self.input_prompt or "", self.extra_prompt)
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
|
|
134
144
|
self.signals.status.emit(trans('img.status.generating') + f": {self.input_prompt}...")
|
|
135
145
|
|
|
136
146
|
cfg = self.window.core.config
|
|
@@ -205,4 +215,17 @@ class ImageWorker(QRunnable):
|
|
|
205
215
|
try:
|
|
206
216
|
sig.deleteLater()
|
|
207
217
|
except RuntimeError:
|
|
208
|
-
pass
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
# ---------- prompt utilities ----------
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def _merge_negative_prompt(prompt: str, negative: Optional[str]) -> str:
|
|
224
|
+
"""
|
|
225
|
+
Append a negative prompt to the main text prompt for providers without a native negative_prompt field.
|
|
226
|
+
"""
|
|
227
|
+
base = (prompt or "").strip()
|
|
228
|
+
neg = (negative or "").strip()
|
|
229
|
+
if not neg:
|
|
230
|
+
return base
|
|
231
|
+
return (base + ("\n" if base else "") + f"Negative prompt: {neg}").strip()
|
|
@@ -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.12.
|
|
9
|
+
# Updated Date: 2025.12.30 22:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import copy
|
|
@@ -207,6 +207,22 @@ class Patch:
|
|
|
207
207
|
patch_css('style.dark.css', True)
|
|
208
208
|
updated = True
|
|
209
209
|
|
|
210
|
+
# < 2.7.3
|
|
211
|
+
if old < parse_version("2.7.3"):
|
|
212
|
+
print("Migrating config from < 2.7.3...")
|
|
213
|
+
if "video.remix" not in data:
|
|
214
|
+
data["video.remix"] = False
|
|
215
|
+
if "img.remix" not in data:
|
|
216
|
+
data["img.remix"] = False
|
|
217
|
+
updated = True
|
|
218
|
+
|
|
219
|
+
# < 2.7.4
|
|
220
|
+
if old < parse_version("2.7.4"):
|
|
221
|
+
print("Migrating config from < 2.7.4...")
|
|
222
|
+
patch_css('web-chatgpt.css', True)
|
|
223
|
+
patch_css('web-blocks.css', True)
|
|
224
|
+
updated = True
|
|
225
|
+
|
|
210
226
|
# update file
|
|
211
227
|
migrated = False
|
|
212
228
|
if updated:
|