@danhachuel/thunderbolt 0.7.39 → 0.7.41
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.
|
@@ -40,7 +40,7 @@ from .provider_routing import (
|
|
|
40
40
|
route_json_request,
|
|
41
41
|
)
|
|
42
42
|
from .storage import STORAGE, ensure_storage, write_json
|
|
43
|
-
from .thumbnail_generation import _compose_thumbnail_prompt, generate_thumbnail_image, normalize_thumbnail_bytes
|
|
43
|
+
from .thumbnail_generation import _compose_thumbnail_prompt, generate_thumbnail_image, infer_thumbnail_aspect_ratio, normalize_thumbnail_bytes
|
|
44
44
|
from .canva_client import CanvaClient
|
|
45
45
|
from .canva_mcp_workflow import run_direct_canva_thumbnail
|
|
46
46
|
|
|
@@ -267,7 +267,14 @@ def _poll_image(card: Mapping[str, Any], request_id: str, *, attempts: int = 24,
|
|
|
267
267
|
raise MediaGenerationError("O provider de imagem não concluiu dentro do limite de polling.")
|
|
268
268
|
|
|
269
269
|
|
|
270
|
-
def _download_or_write(
|
|
270
|
+
def _download_or_write(
|
|
271
|
+
image_bytes: bytes | None,
|
|
272
|
+
url: str,
|
|
273
|
+
destination: Path,
|
|
274
|
+
card: Mapping[str, Any],
|
|
275
|
+
*,
|
|
276
|
+
aspect_ratio: str = "16:9",
|
|
277
|
+
) -> Path:
|
|
271
278
|
if image_bytes is None and url:
|
|
272
279
|
try:
|
|
273
280
|
response = requests.get(url, headers={"Authorization": f"Bearer {_api_key(card)}"} if _api_key(card) else {}, timeout=180)
|
|
@@ -278,7 +285,7 @@ def _download_or_write(image_bytes: bytes | None, url: str, destination: Path, c
|
|
|
278
285
|
if not image_bytes:
|
|
279
286
|
raise MediaGenerationError("O provider concluiu a chamada mas não devolveu uma imagem utilizável.")
|
|
280
287
|
try:
|
|
281
|
-
image_bytes = normalize_thumbnail_bytes(image_bytes)
|
|
288
|
+
image_bytes = normalize_thumbnail_bytes(image_bytes, aspect_ratio)
|
|
282
289
|
except Exception as exc:
|
|
283
290
|
raise MediaGenerationError(str(exc)) from exc
|
|
284
291
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -317,9 +324,8 @@ def _image_endpoint(card: Mapping[str, Any]) -> str:
|
|
|
317
324
|
raise MediaGenerationError(f"O provider {card.get('provider')} não tem endpoint de imagem configurado.")
|
|
318
325
|
|
|
319
326
|
|
|
320
|
-
def _thumbnail_aspect_ratio(thumbnail_blueprint: Mapping[str, Any] | None) -> str:
|
|
321
|
-
|
|
322
|
-
return "9:16" if re.search(r"\b9\s*:\s*16\b|VERTICAL", content, flags=re.IGNORECASE) else "16:9"
|
|
327
|
+
def _thumbnail_aspect_ratio(thumbnail_blueprint: Mapping[str, Any] | None, prompt: str = "") -> str:
|
|
328
|
+
return infer_thumbnail_aspect_ratio(prompt, dict(thumbnail_blueprint or {}))
|
|
323
329
|
|
|
324
330
|
|
|
325
331
|
def _image_request(card: dict[str, Any], prompt: str, *, topic: str = "", lettering_text: str = "", lettering_prompt: str = "", thumbnail_blueprint: Mapping[str, Any] | None = None) -> Any:
|
|
@@ -332,7 +338,7 @@ def _image_request(card: dict[str, Any], prompt: str, *, topic: str = "", letter
|
|
|
332
338
|
lettering_text=lettering_text,
|
|
333
339
|
lettering_prompt=lettering_prompt,
|
|
334
340
|
)
|
|
335
|
-
aspect_ratio = _thumbnail_aspect_ratio(thumbnail_blueprint)
|
|
341
|
+
aspect_ratio = _thumbnail_aspect_ratio(thumbnail_blueprint, prompt)
|
|
336
342
|
requested_size = ("1024x1792" if aspect_ratio == "9:16" else "1792x1024") if provider == "pollinations" else ("720x1280 minimum" if aspect_ratio == "9:16" else "1280x720 minimum")
|
|
337
343
|
constrained_prompt = _append_generation_constraints(
|
|
338
344
|
image_prompt,
|
|
@@ -433,7 +439,7 @@ def generate_image_for_card(
|
|
|
433
439
|
)
|
|
434
440
|
except Exception as exc:
|
|
435
441
|
raise MediaGenerationError(str(exc)) from exc
|
|
436
|
-
aspect_ratio = _thumbnail_aspect_ratio(thumbnail_blueprint)
|
|
442
|
+
aspect_ratio = _thumbnail_aspect_ratio(thumbnail_blueprint, prompt)
|
|
437
443
|
if provider == "nano_banana":
|
|
438
444
|
merged = dict(settings)
|
|
439
445
|
merged["gemini_image_api_key"] = _api_key(card)
|
|
@@ -454,6 +460,7 @@ def generate_image_for_card(
|
|
|
454
460
|
lettering_text=lettering_text,
|
|
455
461
|
lettering_prompt=lettering_prompt,
|
|
456
462
|
reference_image=reference_image,
|
|
463
|
+
aspect_ratio=aspect_ratio,
|
|
457
464
|
)
|
|
458
465
|
except Exception as exc:
|
|
459
466
|
raise MediaGenerationError(str(exc)) from exc
|
|
@@ -486,7 +493,7 @@ def generate_image_for_card(
|
|
|
486
493
|
)
|
|
487
494
|
buffer = io.BytesIO()
|
|
488
495
|
image.save(buffer, format="PNG")
|
|
489
|
-
return _download_or_write(buffer.getvalue(), "", destination, card)
|
|
496
|
+
return _download_or_write(buffer.getvalue(), "", destination, card, aspect_ratio=aspect_ratio)
|
|
490
497
|
except Exception as exc:
|
|
491
498
|
raise MediaGenerationError(f"Hugging Face text-to-image falhou: {str(exc)[:240]}") from exc
|
|
492
499
|
|
|
@@ -514,7 +521,7 @@ def generate_image_for_card(
|
|
|
514
521
|
elif request_id and style == "kie":
|
|
515
522
|
urls = _poll_kie_task(routed.card, request_id, endpoint="/jobs/recordInfo", veo=False)
|
|
516
523
|
url = urls[0] if urls else ""
|
|
517
|
-
return _download_or_write(image_bytes, url, destination, routed.card)
|
|
524
|
+
return _download_or_write(image_bytes, url, destination, routed.card, aspect_ratio=aspect_ratio)
|
|
518
525
|
|
|
519
526
|
|
|
520
527
|
def _is_retryable_media_error(exc: BaseException) -> bool:
|
|
@@ -32,7 +32,7 @@ from hermes_ui.llm_providers import active_llm_card, provider_definition
|
|
|
32
32
|
from hermes_ui.media_generation import MediaGenerationError, _append_generation_constraints, generate_image_from_pool, generate_video_from_pool
|
|
33
33
|
from hermes_ui.media_providers import FULL_IA_VIDEO_PROVIDER_CODES, media_cards_for_pool, media_provider_definition
|
|
34
34
|
from hermes_ui.material_sources import material_api_keys, material_source_cards, selected_material_source
|
|
35
|
-
from hermes_ui.thumbnail_generation import ThumbnailGenerationError, generate_thumbnail_image
|
|
35
|
+
from hermes_ui.thumbnail_generation import ThumbnailGenerationError, generate_thumbnail_image, infer_thumbnail_aspect_ratio
|
|
36
36
|
from hermes_ui.thumbnail_blueprints import thumbnail_blueprint_for_channel
|
|
37
37
|
from hermes_ui.voice_preview import synthesize_preview
|
|
38
38
|
|
|
@@ -97,6 +97,7 @@ def _generate_pipeline_thumbnail(
|
|
|
97
97
|
variant_index=variant_index,
|
|
98
98
|
lettering_text=lettering_text,
|
|
99
99
|
lettering_prompt=lettering_prompt,
|
|
100
|
+
aspect_ratio=infer_thumbnail_aspect_ratio(prompt, thumbnail_blueprint),
|
|
100
101
|
)
|
|
101
102
|
return generate_image_from_pool(
|
|
102
103
|
settings,
|
|
@@ -31,6 +31,22 @@ class ThumbnailGenerationError(RuntimeError):
|
|
|
31
31
|
"""Raised when Nano Banana cannot produce a thumbnail image."""
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def infer_thumbnail_aspect_ratio(
|
|
35
|
+
prompt: str = "",
|
|
36
|
+
blueprint: dict[str, Any] | None = None,
|
|
37
|
+
*,
|
|
38
|
+
fallback: str = DEFAULT_ASPECT_RATIO,
|
|
39
|
+
) -> str:
|
|
40
|
+
"""Resolve the requested thumbnail orientation without trusting provider defaults."""
|
|
41
|
+
blueprint_content = str((blueprint or {}).get("content") or "")
|
|
42
|
+
source = f"{blueprint_content}\n{str(prompt or '')}"
|
|
43
|
+
if re.search(r"\b9\s*:\s*16\b|\bvertical\b|\bportrait\b", source, flags=re.IGNORECASE):
|
|
44
|
+
return "9:16"
|
|
45
|
+
if re.search(r"\b16\s*:\s*9\b|\blandscape\b|\bwide\b", source, flags=re.IGNORECASE):
|
|
46
|
+
return "16:9"
|
|
47
|
+
return fallback if fallback in {"16:9", "9:16"} else DEFAULT_ASPECT_RATIO
|
|
48
|
+
|
|
49
|
+
|
|
34
50
|
def normalize_thumbnail_bytes(image_bytes: bytes, aspect_ratio: str = DEFAULT_ASPECT_RATIO) -> bytes:
|
|
35
51
|
"""Return a non-distorted JPEG in the requested thumbnail orientation."""
|
|
36
52
|
try:
|
|
@@ -188,6 +204,7 @@ def generate_thumbnail_image(
|
|
|
188
204
|
lettering_text=lettering_text,
|
|
189
205
|
lettering_prompt=lettering_prompt,
|
|
190
206
|
)
|
|
207
|
+
effective_aspect_ratio = infer_thumbnail_aspect_ratio(clean_prompt, fallback=aspect_ratio)
|
|
191
208
|
|
|
192
209
|
model = str(settings.get("gemini_image_model") or DEFAULT_GEMINI_IMAGE_MODEL).strip()
|
|
193
210
|
image_size = DEFAULT_IMAGE_SIZE
|
|
@@ -203,7 +220,7 @@ def generate_thumbnail_image(
|
|
|
203
220
|
"response_format": {
|
|
204
221
|
"type": "image",
|
|
205
222
|
"mime_type": DEFAULT_MIME_TYPE,
|
|
206
|
-
"aspect_ratio":
|
|
223
|
+
"aspect_ratio": effective_aspect_ratio,
|
|
207
224
|
"image_size": image_size,
|
|
208
225
|
},
|
|
209
226
|
"store": False,
|
|
@@ -230,7 +247,7 @@ def generate_thumbnail_image(
|
|
|
230
247
|
raise ThumbnailGenerationError("A API Nano Banana devolveu uma resposta que não é JSON.") from exc
|
|
231
248
|
if str(payload.get("status") or "completed").lower() not in {"completed", "succeeded"}:
|
|
232
249
|
raise ThumbnailGenerationError(f"A interação Nano Banana terminou com estado inesperado: {payload.get('status') or 'desconhecido'}.")
|
|
233
|
-
image_bytes = normalize_thumbnail_bytes(_extract_image_bytes(payload),
|
|
250
|
+
image_bytes = normalize_thumbnail_bytes(_extract_image_bytes(payload), effective_aspect_ratio)
|
|
234
251
|
|
|
235
252
|
ensure_storage()
|
|
236
253
|
output_dir = STORAGE / "thumbnails"
|
|
@@ -258,6 +275,7 @@ __all__ = [
|
|
|
258
275
|
"DEFAULT_ASPECT_RATIO",
|
|
259
276
|
"DEFAULT_GEMINI_IMAGE_MODEL",
|
|
260
277
|
"DEFAULT_IMAGE_SIZE",
|
|
278
|
+
"infer_thumbnail_aspect_ratio",
|
|
261
279
|
"THUMBNAIL_HEIGHT",
|
|
262
280
|
"THUMBNAIL_WIDTH",
|
|
263
281
|
"GEMINI_INTERACTIONS_ENDPOINT",
|
package/hermes_ui/thumbnails.py
CHANGED
|
@@ -11,7 +11,7 @@ from .creative_generation import _language_instruction, generate_thumbnail_promp
|
|
|
11
11
|
from .media_generation import generate_image_from_pool
|
|
12
12
|
from .media_providers import media_cards_for_pool
|
|
13
13
|
from .storage import STORAGE, ensure_storage, now, read_json, update_json, write_json
|
|
14
|
-
from .thumbnail_generation import ThumbnailGenerationError, generate_thumbnail_image
|
|
14
|
+
from .thumbnail_generation import ThumbnailGenerationError, generate_thumbnail_image, infer_thumbnail_aspect_ratio
|
|
15
15
|
from .thumbnail_blueprints import thumbnail_blueprint_for_task
|
|
16
16
|
|
|
17
17
|
|
|
@@ -41,7 +41,7 @@ def _generate_image_with_pool(
|
|
|
41
41
|
reference_image=reference_image,
|
|
42
42
|
lettering_text=lettering_text,
|
|
43
43
|
lettering_prompt=lettering_prompt,
|
|
44
|
-
aspect_ratio=
|
|
44
|
+
aspect_ratio=infer_thumbnail_aspect_ratio(prompt, thumbnail_blueprint),
|
|
45
45
|
)
|
|
46
46
|
return generate_image_from_pool(
|
|
47
47
|
settings,
|
package/package.json
CHANGED
package/scripts/cli.mjs
CHANGED
|
@@ -228,7 +228,6 @@ const dynamicChunkRecoveryScript = `<script data-thunderbolt-recovery="${package
|
|
|
228
228
|
const message = String(event.message || "");
|
|
229
229
|
if (/dynamically imported module|module script failed/i.test(message)) recover();
|
|
230
230
|
});
|
|
231
|
-
window.setTimeout(() => { try { sessionStorage.removeItem(key); } catch (_) {} }, 3500);
|
|
232
231
|
})();
|
|
233
232
|
</script>`;
|
|
234
233
|
|
|
@@ -260,7 +259,7 @@ const proxy = http.createServer((request, response) => {
|
|
|
260
259
|
port: backendPort,
|
|
261
260
|
method: request.method,
|
|
262
261
|
path: `${requestUrl.pathname}${requestUrl.search}`,
|
|
263
|
-
headers: { ...request.headers, host: `127.0.0.1:${backendPort}
|
|
262
|
+
headers: { ...request.headers, host: `127.0.0.1:${backendPort}`, "accept-encoding": "identity" },
|
|
264
263
|
}, (upstreamResponse) => {
|
|
265
264
|
const responseHeaders = { ...upstreamResponse.headers };
|
|
266
265
|
// Streamlit entrega HTML/JS de uma instância local que pode mudar após
|