@danhachuel/thunderbolt 0.7.39 → 0.7.40

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(image_bytes: bytes | None, url: str, destination: Path, card: Mapping[str, Any]) -> Path:
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
- content = str((thumbnail_blueprint or {}).get("content") or "")
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": 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), aspect_ratio)
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",
@@ -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="9:16" if "9:16" in str((thumbnail_blueprint or {}).get("content") or "") else "16:9",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.7.39",
3
+ "version": "0.7.40",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",