@danhachuel/thunderbolt 0.3.41 → 0.3.43
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.
- package/MANUAL-INSTALACAO.md +47 -12
- package/README.md +21 -9
- package/app/main.py +397 -193
- package/hermes_ui/api_key_tests.py +33 -1
- package/hermes_ui/creative_generation.py +18 -26
- package/hermes_ui/domain.py +15 -6
- package/hermes_ui/languages.py +8 -2
- package/hermes_ui/llm_providers.py +4 -0
- package/hermes_ui/media_generation.py +383 -0
- package/hermes_ui/media_providers.py +282 -0
- package/hermes_ui/pipeline_worker.py +64 -4
- package/hermes_ui/provider_routing.py +457 -0
- package/hermes_ui/storage.py +32 -8
- package/hermes_ui/thumbnail_generation.py +0 -1
- package/hermes_ui/thumbnails.py +42 -3
- package/package.json +1 -1
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Configuration schema for independent image and video provider pools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Mapping
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
MEDIA_CARDS_KEY = "media_provider_cards"
|
|
10
|
+
MEDIA_IMAGE_ACTIVE_CARD_KEY = "media_image_active_card_id"
|
|
11
|
+
MEDIA_VIDEO_ACTIVE_CARD_KEY = "media_video_active_card_id"
|
|
12
|
+
MEDIA_IMAGE_ACTIVE_PROVIDER_KEY = "media_image_provider"
|
|
13
|
+
MEDIA_VIDEO_ACTIVE_PROVIDER_KEY = "media_video_provider"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class MediaProviderDefinition:
|
|
18
|
+
code: str
|
|
19
|
+
label: str
|
|
20
|
+
default_base_url: str = ""
|
|
21
|
+
requires_api_key: bool = True
|
|
22
|
+
supports_image: bool = False
|
|
23
|
+
supports_video: bool = False
|
|
24
|
+
supports_text: bool = False
|
|
25
|
+
local: bool = False
|
|
26
|
+
api_style: str = "custom"
|
|
27
|
+
extra_fields: tuple[str, ...] = ()
|
|
28
|
+
description: str = ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
MEDIA_PROVIDER_CATALOG: tuple[MediaProviderDefinition, ...] = (
|
|
32
|
+
MediaProviderDefinition(
|
|
33
|
+
"nano_banana",
|
|
34
|
+
"Nano Banana",
|
|
35
|
+
default_base_url="https://generativelanguage.googleapis.com/v1beta",
|
|
36
|
+
supports_image=True,
|
|
37
|
+
api_style="gemini_interactions",
|
|
38
|
+
extra_fields=("aspect_ratio", "image_size"),
|
|
39
|
+
description="Geração de imagem nativa Gemini para thumbnails e artes.",
|
|
40
|
+
),
|
|
41
|
+
MediaProviderDefinition(
|
|
42
|
+
"pollinations",
|
|
43
|
+
"Pollinations.ai",
|
|
44
|
+
default_base_url="https://gen.pollinations.ai/v1",
|
|
45
|
+
supports_image=True,
|
|
46
|
+
supports_video=True,
|
|
47
|
+
supports_text=True,
|
|
48
|
+
api_style="openai_compatible",
|
|
49
|
+
description="Gateway multimodal com endpoints compatíveis e catálogo próprio.",
|
|
50
|
+
),
|
|
51
|
+
MediaProviderDefinition(
|
|
52
|
+
"agnes",
|
|
53
|
+
"Agnes AI",
|
|
54
|
+
default_base_url="https://apihub.agnes-ai.com/v1",
|
|
55
|
+
supports_image=True,
|
|
56
|
+
supports_video=True,
|
|
57
|
+
supports_text=True,
|
|
58
|
+
api_style="agnes",
|
|
59
|
+
description="Gateway multimodal Agnes para texto, imagem e vídeo.",
|
|
60
|
+
),
|
|
61
|
+
MediaProviderDefinition(
|
|
62
|
+
"huggingface",
|
|
63
|
+
"Hugging Face Inference API",
|
|
64
|
+
default_base_url="https://router.huggingface.co/v1",
|
|
65
|
+
supports_image=True,
|
|
66
|
+
supports_text=True,
|
|
67
|
+
api_style="huggingface",
|
|
68
|
+
description="Inference Providers; a capacidade depende do modelo seleccionado.",
|
|
69
|
+
),
|
|
70
|
+
MediaProviderDefinition(
|
|
71
|
+
"cloudflare_workers_ai",
|
|
72
|
+
"Cloudflare Workers AI",
|
|
73
|
+
default_base_url="https://api.cloudflare.com/client/v4",
|
|
74
|
+
supports_image=True,
|
|
75
|
+
api_style="cloudflare",
|
|
76
|
+
extra_fields=("account_id",),
|
|
77
|
+
description="Workers AI com Account ID e API token; a rota de modelo é derivada.",
|
|
78
|
+
),
|
|
79
|
+
MediaProviderDefinition(
|
|
80
|
+
"inferenceport",
|
|
81
|
+
"InferencePort Proxy",
|
|
82
|
+
default_base_url="http://localhost:8080/v1",
|
|
83
|
+
requires_api_key=False,
|
|
84
|
+
supports_image=True,
|
|
85
|
+
supports_video=True,
|
|
86
|
+
supports_text=True,
|
|
87
|
+
local=True,
|
|
88
|
+
api_style="openai_compatible",
|
|
89
|
+
description="Proxy local OpenAI-compatible; não exige API key por defeito.",
|
|
90
|
+
),
|
|
91
|
+
MediaProviderDefinition(
|
|
92
|
+
"alibaba_cloud",
|
|
93
|
+
"阿里云 (Alibaba Cloud Model Studio)",
|
|
94
|
+
default_base_url="https://dashscope-intl.aliyuncs.com/api/v1",
|
|
95
|
+
supports_image=True,
|
|
96
|
+
supports_video=True,
|
|
97
|
+
supports_text=True,
|
|
98
|
+
api_style="dashscope",
|
|
99
|
+
extra_fields=("region",),
|
|
100
|
+
description="DashScope/Model Studio; tarefas de imagem e vídeo podem ser assíncronas.",
|
|
101
|
+
),
|
|
102
|
+
MediaProviderDefinition(
|
|
103
|
+
"kie_ai",
|
|
104
|
+
"KIE AI",
|
|
105
|
+
default_base_url="https://api.kie.ai/api/v1",
|
|
106
|
+
supports_image=True,
|
|
107
|
+
supports_video=True,
|
|
108
|
+
supports_text=True,
|
|
109
|
+
api_style="kie",
|
|
110
|
+
description="Gateway multimodal com tarefas e consulta de resultados.",
|
|
111
|
+
),
|
|
112
|
+
MediaProviderDefinition(
|
|
113
|
+
"fal_ai",
|
|
114
|
+
"FAL AI",
|
|
115
|
+
default_base_url="https://queue.fal.run",
|
|
116
|
+
supports_image=True,
|
|
117
|
+
supports_video=True,
|
|
118
|
+
api_style="fal_queue",
|
|
119
|
+
description="Model APIs da FAL com queue, status e resultado.",
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
_MEDIA_BY_CODE = {item.code: item for item in MEDIA_PROVIDER_CATALOG}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def media_provider_catalog() -> list[dict[str, Any]]:
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
"code": item.code,
|
|
130
|
+
"label": item.label,
|
|
131
|
+
"default_base_url": item.default_base_url,
|
|
132
|
+
"requires_api_key": item.requires_api_key,
|
|
133
|
+
"supports_image": item.supports_image,
|
|
134
|
+
"supports_video": item.supports_video,
|
|
135
|
+
"supports_text": item.supports_text,
|
|
136
|
+
"local": item.local,
|
|
137
|
+
"api_style": item.api_style,
|
|
138
|
+
"extra_fields": item.extra_fields,
|
|
139
|
+
"description": item.description,
|
|
140
|
+
}
|
|
141
|
+
for item in MEDIA_PROVIDER_CATALOG
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def media_provider_definition(provider: Any) -> MediaProviderDefinition:
|
|
146
|
+
code = str(provider or "").strip().lower()
|
|
147
|
+
return _MEDIA_BY_CODE.get(code, _MEDIA_BY_CODE["inferenceport"])
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def normalize_media_card(card: Any, index: int = 0) -> dict[str, Any]:
|
|
151
|
+
source = dict(card) if isinstance(card, Mapping) else {}
|
|
152
|
+
provider = str(source.get("provider") or "nano_banana").strip().lower()
|
|
153
|
+
definition = media_provider_definition(provider)
|
|
154
|
+
if provider not in _MEDIA_BY_CODE:
|
|
155
|
+
provider = definition.code
|
|
156
|
+
card_id = str(source.get("id") or f"media-{provider}-{index + 1}").strip()
|
|
157
|
+
result: dict[str, Any] = {
|
|
158
|
+
"id": card_id,
|
|
159
|
+
"provider": provider,
|
|
160
|
+
"api_key": str(source.get("api_key") or source.get("key") or "").strip(),
|
|
161
|
+
"model": str(source.get("model") or source.get("model_name") or "").strip(),
|
|
162
|
+
"base_url": str(source.get("base_url") or "").strip() or definition.default_base_url,
|
|
163
|
+
"enabled": bool(source.get("enabled", True)),
|
|
164
|
+
"priority": max(0, int(source.get("priority", index)) if str(source.get("priority", index)).strip().lstrip("-").isdigit() else index),
|
|
165
|
+
"supports_image": bool(source.get("supports_image", definition.supports_image)),
|
|
166
|
+
"supports_video": bool(source.get("supports_video", definition.supports_video)),
|
|
167
|
+
"supports_text": bool(source.get("supports_text", definition.supports_text)),
|
|
168
|
+
"api_style": definition.api_style,
|
|
169
|
+
"local": definition.local,
|
|
170
|
+
}
|
|
171
|
+
for field in definition.extra_fields:
|
|
172
|
+
result[field] = str(source.get(field) or "").strip()
|
|
173
|
+
test_result = source.get("test_result")
|
|
174
|
+
if isinstance(test_result, Mapping) and str(test_result.get("status") or "") in {"success", "error"}:
|
|
175
|
+
result["test_result"] = {
|
|
176
|
+
"status": str(test_result.get("status")),
|
|
177
|
+
"message": str(test_result.get("message") or "")[:240],
|
|
178
|
+
"tested_at": str(test_result.get("tested_at") or "")[:64],
|
|
179
|
+
}
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def new_media_card(provider: Any, *, card_id: str | None = None) -> dict[str, Any]:
|
|
184
|
+
code = str(provider or "").strip().lower()
|
|
185
|
+
if code not in _MEDIA_BY_CODE:
|
|
186
|
+
raise ValueError("Provider de imagem/vídeo inválido.")
|
|
187
|
+
definition = _MEDIA_BY_CODE[code]
|
|
188
|
+
return normalize_media_card(
|
|
189
|
+
{
|
|
190
|
+
"id": card_id or f"media-{code}-1",
|
|
191
|
+
"provider": code,
|
|
192
|
+
"model": "gemini-3.1-flash-image" if code == "nano_banana" else "",
|
|
193
|
+
"base_url": definition.default_base_url,
|
|
194
|
+
"enabled": True,
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def ensure_media_provider_cards(settings: Mapping[str, Any]) -> tuple[dict[str, Any], bool]:
|
|
200
|
+
result = dict(settings)
|
|
201
|
+
raw_cards = result.get(MEDIA_CARDS_KEY)
|
|
202
|
+
changed = False
|
|
203
|
+
if isinstance(raw_cards, list) and raw_cards:
|
|
204
|
+
cards = [normalize_media_card(item, index) for index, item in enumerate(raw_cards)]
|
|
205
|
+
changed = cards != raw_cards
|
|
206
|
+
else:
|
|
207
|
+
cards: list[dict[str, Any]] = []
|
|
208
|
+
legacy_key = str(result.get("gemini_image_api_key") or "").strip()
|
|
209
|
+
legacy_model = str(result.get("gemini_image_model") or "gemini-3.1-flash-image").strip()
|
|
210
|
+
cards.append(
|
|
211
|
+
normalize_media_card(
|
|
212
|
+
{
|
|
213
|
+
"id": "media-nano-banana-default",
|
|
214
|
+
"provider": "nano_banana",
|
|
215
|
+
"api_key": legacy_key,
|
|
216
|
+
"model": legacy_model,
|
|
217
|
+
"base_url": "https://generativelanguage.googleapis.com/v1beta",
|
|
218
|
+
"aspect_ratio": str(result.get("gemini_image_aspect_ratio") or "16:9"),
|
|
219
|
+
"image_size": str(result.get("gemini_image_size") or "1K"),
|
|
220
|
+
"enabled": True,
|
|
221
|
+
},
|
|
222
|
+
0,
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
changed = True
|
|
226
|
+
result[MEDIA_CARDS_KEY] = cards
|
|
227
|
+
|
|
228
|
+
for pool, key, capability in (
|
|
229
|
+
("image", MEDIA_IMAGE_ACTIVE_CARD_KEY, "supports_image"),
|
|
230
|
+
("video", MEDIA_VIDEO_ACTIVE_CARD_KEY, "supports_video"),
|
|
231
|
+
):
|
|
232
|
+
active_id = str(result.get(key) or "").strip()
|
|
233
|
+
valid = [card for card in cards if card.get(capability) and card.get("enabled", True)]
|
|
234
|
+
if active_id not in {str(card.get("id")) for card in cards} or not any(str(card.get("id")) == active_id and card.get(capability) and card.get("enabled", True) for card in cards):
|
|
235
|
+
active_id = str(valid[0].get("id")) if valid else ""
|
|
236
|
+
if result.get(key) != active_id:
|
|
237
|
+
result[key] = active_id
|
|
238
|
+
changed = True
|
|
239
|
+
legacy_provider_key = MEDIA_IMAGE_ACTIVE_PROVIDER_KEY if pool == "image" else MEDIA_VIDEO_ACTIVE_PROVIDER_KEY
|
|
240
|
+
active_card = next((card for card in cards if str(card.get("id")) == active_id), None)
|
|
241
|
+
active_provider = str(active_card.get("provider") or "") if active_card else ""
|
|
242
|
+
if result.get(legacy_provider_key) != active_provider:
|
|
243
|
+
result[legacy_provider_key] = active_provider
|
|
244
|
+
changed = True
|
|
245
|
+
return result, changed
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def apply_media_provider_cards_to_settings(settings: Mapping[str, Any], cards: list[Mapping[str, Any]], image_active_id: str = "", video_active_id: str = "") -> dict[str, Any]:
|
|
249
|
+
result = dict(settings)
|
|
250
|
+
normalized = [normalize_media_card(item, index) for index, item in enumerate(cards)]
|
|
251
|
+
if not normalized:
|
|
252
|
+
normalized = [new_media_card("nano_banana", card_id="media-nano-banana-default")]
|
|
253
|
+
result[MEDIA_CARDS_KEY] = normalized
|
|
254
|
+
for key, wanted, capability in (
|
|
255
|
+
(MEDIA_IMAGE_ACTIVE_CARD_KEY, image_active_id, "supports_image"),
|
|
256
|
+
(MEDIA_VIDEO_ACTIVE_CARD_KEY, video_active_id, "supports_video"),
|
|
257
|
+
):
|
|
258
|
+
selected = next((card for card in normalized if str(card.get("id")) == str(wanted) and card.get(capability) and card.get("enabled", True)), None)
|
|
259
|
+
if selected is None:
|
|
260
|
+
selected = next((card for card in normalized if card.get(capability) and card.get("enabled", True)), None)
|
|
261
|
+
result[key] = str(selected.get("id")) if selected else ""
|
|
262
|
+
image_card = next((card for card in normalized if str(card.get("id")) == result[MEDIA_IMAGE_ACTIVE_CARD_KEY]), None)
|
|
263
|
+
video_card = next((card for card in normalized if str(card.get("id")) == result[MEDIA_VIDEO_ACTIVE_CARD_KEY]), None)
|
|
264
|
+
result[MEDIA_IMAGE_ACTIVE_PROVIDER_KEY] = str(image_card.get("provider") or "") if image_card else ""
|
|
265
|
+
result[MEDIA_VIDEO_ACTIVE_PROVIDER_KEY] = str(video_card.get("provider") or "") if video_card else ""
|
|
266
|
+
nano_card = next((card for card in normalized if card.get("provider") == "nano_banana"), None)
|
|
267
|
+
if nano_card:
|
|
268
|
+
result["gemini_image_api_key"] = str(nano_card.get("api_key") or "")
|
|
269
|
+
result["gemini_image_model"] = str(nano_card.get("model") or "gemini-3.1-flash-image")
|
|
270
|
+
result["gemini_image_aspect_ratio"] = str(nano_card.get("aspect_ratio") or result.get("gemini_image_aspect_ratio") or "16:9")
|
|
271
|
+
result["gemini_image_size"] = str(nano_card.get("image_size") or result.get("gemini_image_size") or "1K")
|
|
272
|
+
return result
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def media_cards_for_pool(settings: Mapping[str, Any], pool: str) -> list[dict[str, Any]]:
|
|
276
|
+
migrated, _ = ensure_media_provider_cards(settings)
|
|
277
|
+
capability = "supports_image" if pool == "image" else "supports_video" if pool == "video" else "supports_text"
|
|
278
|
+
active_key = MEDIA_IMAGE_ACTIVE_CARD_KEY if pool == "image" else MEDIA_VIDEO_ACTIVE_CARD_KEY if pool == "video" else ""
|
|
279
|
+
active_id = str(migrated.get(active_key) or "")
|
|
280
|
+
cards = [dict(item) for item in migrated.get(MEDIA_CARDS_KEY, []) if item.get("enabled", True) and item.get(capability)]
|
|
281
|
+
cards.sort(key=lambda card: (0 if str(card.get("id")) == active_id else 1, int(card.get("priority", 0))))
|
|
282
|
+
return cards
|
|
@@ -17,6 +17,8 @@ from hermes_ui.script_documents import save_script_document
|
|
|
17
17
|
from hermes_ui.script_generation import generate_script_document
|
|
18
18
|
from hermes_ui.storage import STORAGE, ensure_storage, read_json, write_json
|
|
19
19
|
from hermes_ui.llm_providers import active_llm_card, provider_definition
|
|
20
|
+
from hermes_ui.media_generation import MediaGenerationError, generate_image_from_pool, generate_video_from_pool
|
|
21
|
+
from hermes_ui.media_providers import media_cards_for_pool
|
|
20
22
|
from hermes_ui.thumbnail_generation import generate_thumbnail_image
|
|
21
23
|
|
|
22
24
|
PIPELINE_LOCK_FILENAME = "pipeline_worker.lock"
|
|
@@ -43,6 +45,36 @@ def _settings() -> dict[str, Any]:
|
|
|
43
45
|
return value if isinstance(value, dict) else {}
|
|
44
46
|
|
|
45
47
|
|
|
48
|
+
def _generate_pipeline_thumbnail(
|
|
49
|
+
settings: dict[str, Any],
|
|
50
|
+
prompt: str,
|
|
51
|
+
*,
|
|
52
|
+
topic: str,
|
|
53
|
+
variant_index: int = 0,
|
|
54
|
+
lettering_text: str = "",
|
|
55
|
+
lettering_prompt: str = "",
|
|
56
|
+
) -> Path:
|
|
57
|
+
"""Use the image pool while keeping the legacy single-Nano call seam."""
|
|
58
|
+
cards = media_cards_for_pool(settings, "image")
|
|
59
|
+
if len(cards) == 1 and str(cards[0].get("provider") or "") == "nano_banana":
|
|
60
|
+
return generate_thumbnail_image(
|
|
61
|
+
settings,
|
|
62
|
+
prompt,
|
|
63
|
+
topic=topic,
|
|
64
|
+
variant_index=variant_index,
|
|
65
|
+
lettering_text=lettering_text,
|
|
66
|
+
lettering_prompt=lettering_prompt,
|
|
67
|
+
)
|
|
68
|
+
return generate_image_from_pool(
|
|
69
|
+
settings,
|
|
70
|
+
prompt,
|
|
71
|
+
topic=topic,
|
|
72
|
+
variant_index=variant_index,
|
|
73
|
+
lettering_text=lettering_text,
|
|
74
|
+
lettering_prompt=lettering_prompt,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
46
78
|
def _lock_path() -> Path:
|
|
47
79
|
ensure_storage()
|
|
48
80
|
return STORAGE / "state" / PIPELINE_LOCK_FILENAME
|
|
@@ -470,8 +502,26 @@ def _run_task(task: dict[str, Any]) -> dict[str, Any]:
|
|
|
470
502
|
_update(task_id, artifacts=artifacts, progress=30)
|
|
471
503
|
|
|
472
504
|
_update(task_id, stage="title", state="doing", progress=35)
|
|
473
|
-
|
|
474
|
-
|
|
505
|
+
provided_title = str(task.get("title") or "").strip()
|
|
506
|
+
existing_variant = task.get("thumbnail_variant") if isinstance(task.get("thumbnail_variant"), dict) else {}
|
|
507
|
+
existing_variant = dict(existing_variant)
|
|
508
|
+
if not str(existing_variant.get("image_prompt") or "").strip() and str(task.get("thumbnail_prompt") or "").strip():
|
|
509
|
+
existing_variant["image_prompt"] = str(task.get("thumbnail_prompt") or "").strip()
|
|
510
|
+
if not str(existing_variant.get("overlay_text") or "").strip() and str(task.get("thumbnail_text") or "").strip():
|
|
511
|
+
existing_variant["overlay_text"] = str(task.get("thumbnail_text") or "").strip()
|
|
512
|
+
prepared_thumbnail = bool(str(existing_variant.get("image_prompt") or "").strip())
|
|
513
|
+
if provided_title and prepared_thumbnail:
|
|
514
|
+
# The creation UI already generated the title/thumbnail brief. Do not call
|
|
515
|
+
# the complete creative-package generator a second time for this task.
|
|
516
|
+
creative = {
|
|
517
|
+
"title": provided_title,
|
|
518
|
+
"keywords": [],
|
|
519
|
+
"thumbnail_variant": existing_variant,
|
|
520
|
+
"title_candidates": task.get("title_candidates") if isinstance(task.get("title_candidates"), list) else [],
|
|
521
|
+
}
|
|
522
|
+
else:
|
|
523
|
+
creative = generate_creative_package(settings, channel, topic, blueprint, language=str(task.get("language") or channel.get("language") or "Português"))
|
|
524
|
+
title = str(creative.get("title") or provided_title or topic).strip()
|
|
475
525
|
provided_keywords = generation_settings.get("video_keywords")
|
|
476
526
|
if isinstance(provided_keywords, str):
|
|
477
527
|
provided_keywords = re.split(r"[,\n;|]+", provided_keywords)
|
|
@@ -482,6 +532,8 @@ def _run_task(task: dict[str, Any]) -> dict[str, Any]:
|
|
|
482
532
|
|
|
483
533
|
_update(task_id, stage="keywords", state="doing", progress=48)
|
|
484
534
|
variant = creative.get("thumbnail_variant") if isinstance(creative.get("thumbnail_variant"), dict) else {}
|
|
535
|
+
if not variant and prepared_thumbnail:
|
|
536
|
+
variant = existing_variant
|
|
485
537
|
prompt_payload = {
|
|
486
538
|
"topic": topic,
|
|
487
539
|
"title": title,
|
|
@@ -501,7 +553,7 @@ def _run_task(task: dict[str, Any]) -> dict[str, Any]:
|
|
|
501
553
|
artifacts = {**artifacts, "thumbnail_prompt_json": prompt_artifact}
|
|
502
554
|
_update(task_id, stage="thumbnail_prompt", state="doing", progress=52, thumbnail_prompt=str(variant.get("image_prompt") or ""), thumbnail_text=str(variant.get("overlay_text") or ""), thumbnail_status="prompt_ready", artifacts=artifacts)
|
|
503
555
|
_update(task_id, stage="thumbnail", state="doing", progress=56, thumbnail_prompt=str(variant.get("image_prompt") or ""), thumbnail_text=str(variant.get("overlay_text") or ""), thumbnail_status="prompt_ready", artifacts=artifacts)
|
|
504
|
-
thumbnail_path =
|
|
556
|
+
thumbnail_path = _generate_pipeline_thumbnail(
|
|
505
557
|
settings,
|
|
506
558
|
str(variant.get("image_prompt") or ""),
|
|
507
559
|
topic=topic,
|
|
@@ -513,7 +565,15 @@ def _run_task(task: dict[str, Any]) -> dict[str, Any]:
|
|
|
513
565
|
_update(task_id, artifacts=artifacts, thumbnail_status="generated", progress=62)
|
|
514
566
|
|
|
515
567
|
_update(task_id, stage="video", state="doing", progress=68)
|
|
516
|
-
|
|
568
|
+
video_cards = media_cards_for_pool(settings, "video")
|
|
569
|
+
if bool(settings.get("media_video_pool_enabled")) and video_cards:
|
|
570
|
+
try:
|
|
571
|
+
video_prompt = f"Título: {title}\n\nRoteiro:\n{str(script.get('content') or '')[:12000]}"
|
|
572
|
+
video_path = generate_video_from_pool(settings, video_prompt)
|
|
573
|
+
except MediaGenerationError as exc:
|
|
574
|
+
raise PipelineError(f"Pool de vídeo externo: {exc}") from exc
|
|
575
|
+
else:
|
|
576
|
+
video_path = _run_video_helper({**task, "topic": topic})
|
|
517
577
|
artifacts["video"] = str(video_path)
|
|
518
578
|
_update(task_id, artifacts=artifacts, progress=80)
|
|
519
579
|
|