@danhachuel/thunderbolt 0.4.91 → 0.4.93

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/app/main.py CHANGED
@@ -4,12 +4,16 @@ import io
4
4
 
5
5
  os.environ["PYTHONIOENCODING"] = "utf-8"
6
6
  os.environ["PYTHONUTF8"] = "1"
7
+ os.environ["CLICK_NO_WIN_CONSOLE"] = "1"
7
8
 
8
9
  def _force_utf8_stream(stream: object) -> object:
10
+ if os.name != "nt":
11
+ return stream
9
12
  buffer = getattr(stream, "buffer", None)
10
- encoding = str(getattr(stream, "encoding", "") or "").lower().replace("-", "_")
11
- if buffer is not None and encoding != "utf_8":
12
- return io.TextIOWrapper(buffer, encoding="utf-8", errors="replace", line_buffering=True)
13
+ if buffer is not None and not getattr(stream, "_thunderbolt_utf8", False):
14
+ wrapped = io.TextIOWrapper(buffer, encoding="utf-8", errors="replace", line_buffering=True)
15
+ setattr(wrapped, "_thunderbolt_utf8", True)
16
+ return wrapped
13
17
  return stream
14
18
 
15
19
  _original_stdout, _original_stderr = sys.stdout, sys.stderr
@@ -102,6 +106,7 @@ from hermes_ui.thumbnails import (
102
106
  from hermes_ui.draft_video import DRAFT_SETTING_SECTIONS, missing_content_fields, missing_setting_sections, normalise_saved_script, setting_widget_suffixes
103
107
  from hermes_ui.creative_generation import CreativeGenerationError, generate_creative_package, generate_thumbnail_prompt, generate_title_and_keywords, generate_topic_for_channel, generate_video_description, generate_video_keywords, generate_video_update_metadata
104
108
  from hermes_ui.media_generation import MediaGenerationError, format_media_generation_error, generate_image_for_card, generate_video_for_card
109
+ from hermes_ui.canva_auth import authorization_url, create_pkce_pair, create_state, exchange_code
105
110
  from integrations.platforms import IntegrationResult, TikTokAdapter, YouTubeAdapter, fetch_channel_videos_public
106
111
  from integrations.tiktok_public import fetch_public_tiktok_profile, normalize_tiktok_reference
107
112
  from integrations.youtube_update import YOUTUBE_UPDATE_VIDEO, YouTubeVideoUpdater
@@ -7060,7 +7065,7 @@ def _media_card_config_status(card: dict[str, Any]) -> tuple[str, str]:
7060
7065
  return "missing", "Missing key"
7061
7066
  if not str(card.get("base_url") or "").strip():
7062
7067
  return "missing", "Missing Base URL"
7063
- if not str(card.get("model") or "").strip() and card.get("supports_image") and card.get("provider") not in {"cloudflare_workers_ai"}:
7068
+ if not str(card.get("model") or "").strip() and card.get("supports_image") and card.get("provider") not in {"cloudflare_workers_ai", "canva"}:
7064
7069
  return "missing", "Missing model"
7065
7070
  return "ready", "Configured"
7066
7071
 
@@ -7096,6 +7101,26 @@ def _render_media_provider_card(settings: dict[str, Any], cards: list[dict[str,
7096
7101
  cards[index] = card
7097
7102
  card_id = str(card["id"])
7098
7103
  definition = media_provider_definition(card.get("provider"))
7104
+ if card.get("provider") == "canva":
7105
+ callback_code = str(st.query_params.get("code") or "").strip()
7106
+ callback_state = str(st.query_params.get("state") or "").strip()
7107
+ pending_state = str(st.session_state.get(f"canva_state_{card_id}") or "")
7108
+ if callback_code:
7109
+ if callback_state != pending_state:
7110
+ st.error("Canva OAuth rejeitado: state inválido. Inicie a autorização novamente.")
7111
+ else:
7112
+ try:
7113
+ token = exchange_code(str(card.get("client_id") or ""), str(card.get("client_secret") or ""), callback_code, str(card.get("redirect_uri") or ""), str(st.session_state.get(f"canva_verifier_{card_id}") or ""))
7114
+ card["oauth_token"] = token
7115
+ cards[index] = card
7116
+ _persist_media_cards(settings, cards, str(settings.get(MEDIA_IMAGE_ACTIVE_CARD_KEY) or ""), str(settings.get(MEDIA_VIDEO_ACTIVE_CARD_KEY) or ""))
7117
+ st.session_state.pop(f"canva_state_{card_id}", None)
7118
+ st.session_state.pop(f"canva_verifier_{card_id}", None)
7119
+ st.query_params.clear()
7120
+ st.success("Canva autorizada com sucesso.")
7121
+ st.rerun()
7122
+ except Exception as exc:
7123
+ st.error(f"Não foi possível autorizar a Canva: {str(exc)[:240]}")
7099
7124
  with st.container(border=True):
7100
7125
  header_cols = st.columns([3.2, 1.2])
7101
7126
  with header_cols[0]:
@@ -7139,14 +7164,29 @@ def _render_media_provider_card(settings: dict[str, Any], cards: list[dict[str,
7139
7164
  extra_cols = st.columns(len(definition.extra_fields))
7140
7165
  for extra_col, field_name in zip(extra_cols, definition.extra_fields):
7141
7166
  with extra_col:
7142
- extra_values[field_name] = st.text_input(field_name.replace("_", " ").title(), value=str(card.get(field_name) or ""), key=f"media_card_{card_id}_{field_name}")
7167
+ extra_values[field_name] = st.text_input(field_name.replace("_", " ").title(), value=str(card.get(field_name) or ""), type="password" if field_name == "client_secret" else "default", key=f"media_card_{card_id}_{field_name}")
7168
+ if definition.code == "canva":
7169
+ supports_image = True
7170
+ supports_video = False
7171
+ st.caption("Canva Connect é usado exclusivamente para thumbnails no Pool de Imagem. Não é provider de vídeo.")
7172
+ redirect_uri = str(card.get("redirect_uri") or "").strip()
7173
+ if not redirect_uri:
7174
+ st.info("Defina o Redirect URI no Developer Portal Canva e repita-o neste card antes de autorizar.")
7175
+ elif str(card.get("client_id") or "").strip() and str(card.get("client_secret") or "").strip():
7176
+ if st.button("Autorizar Canva", key=f"media_card_{card_id}_authorize"):
7177
+ verifier, challenge = create_pkce_pair()
7178
+ state = create_state()
7179
+ st.session_state[f"canva_verifier_{card_id}"] = verifier
7180
+ st.session_state[f"canva_state_{card_id}"] = state
7181
+ st.link_button("Abrir autorização Canva", authorization_url(str(card.get("client_id")), redirect_uri, "design:content:read design:content:write", state, challenge), use_container_width=True)
7182
+ st.caption("Estado OAuth: autorizado" if (card.get("oauth_token") or {}).get("access_token") else "Estado OAuth: não autorizado")
7143
7183
  status_cols = st.columns(4)
7144
7184
  with status_cols[0]:
7145
7185
  enabled = st.checkbox("Provider activo", value=bool(card.get("enabled", True)), key=f"media_card_{card_id}_enabled")
7146
7186
  with status_cols[1]:
7147
7187
  supports_image = st.checkbox("Pool Imagem", value=bool(card.get("supports_image", definition.supports_image)), key=f"media_card_{card_id}_image")
7148
7188
  with status_cols[2]:
7149
- supports_video = st.checkbox("Pool Vídeo", value=bool(card.get("supports_video", definition.supports_video)), key=f"media_card_{card_id}_video")
7189
+ supports_video = False if definition.code == "canva" else st.checkbox("Pool Vídeo", value=bool(card.get("supports_video", definition.supports_video)), key=f"media_card_{card_id}_video")
7150
7190
  with status_cols[3]:
7151
7191
  priority = st.number_input("Prioridade", min_value=0, max_value=999, value=int(card.get("priority", index)), step=1, key=f"media_card_{card_id}_priority")
7152
7192
  action_cols = st.columns(4)
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import hashlib
5
+ import secrets
6
+ import time
7
+ from typing import Any, Mapping
8
+ from urllib.parse import urlencode
9
+
10
+ import requests
11
+
12
+ AUTHORIZE_URL = "https://www.canva.com/api/oauth/authorize"
13
+ TOKEN_URL = "https://api.canva.com/rest/v1/oauth/token"
14
+
15
+
16
+ def _b64(value: bytes) -> str:
17
+ return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii")
18
+
19
+
20
+ def create_pkce_pair() -> tuple[str, str]:
21
+ verifier = _b64(secrets.token_bytes(64))
22
+ challenge = _b64(hashlib.sha256(verifier.encode("ascii")).digest())
23
+ return verifier, challenge
24
+
25
+
26
+ def create_state() -> str:
27
+ return _b64(secrets.token_bytes(48))
28
+
29
+
30
+ def authorization_url(client_id: str, redirect_uri: str, scope: str, state: str, code_challenge: str) -> str:
31
+ params = {
32
+ "code_challenge": code_challenge,
33
+ "code_challenge_method": "S256",
34
+ "scope": " ".join(str(scope or "").split()),
35
+ "response_type": "code",
36
+ "client_id": str(client_id).strip(),
37
+ "state": str(state).strip(),
38
+ "redirect_uri": str(redirect_uri).strip(),
39
+ }
40
+ return f"{AUTHORIZE_URL}?{urlencode(params)}"
41
+
42
+
43
+ def exchange_code(client_id: str, client_secret: str, code: str, redirect_uri: str, code_verifier: str, *, timeout: int = 30) -> dict[str, Any]:
44
+ response = requests.post(
45
+ TOKEN_URL,
46
+ data={
47
+ "grant_type": "authorization_code",
48
+ "code": code,
49
+ "code_verifier": code_verifier,
50
+ "client_id": client_id,
51
+ "client_secret": client_secret,
52
+ "redirect_uri": redirect_uri,
53
+ },
54
+ headers={"Accept": "application/json"},
55
+ timeout=timeout,
56
+ )
57
+ if response.status_code >= 400:
58
+ raise RuntimeError(f"Canva OAuth devolveu HTTP {response.status_code}: {response.text[:240]}")
59
+ payload = response.json()
60
+ if not payload.get("access_token"):
61
+ raise RuntimeError("Canva OAuth não devolveu access_token.")
62
+ return _with_expiry(payload)
63
+
64
+
65
+ def refresh_access_token(client_id: str, client_secret: str, refresh_token: str, *, timeout: int = 30) -> dict[str, Any]:
66
+ response = requests.post(
67
+ TOKEN_URL,
68
+ data={
69
+ "grant_type": "refresh_token",
70
+ "refresh_token": refresh_token,
71
+ "client_id": client_id,
72
+ "client_secret": client_secret,
73
+ },
74
+ headers={"Accept": "application/json"},
75
+ timeout=timeout,
76
+ )
77
+ if response.status_code >= 400:
78
+ raise RuntimeError(f"Refresh Canva devolveu HTTP {response.status_code}: {response.text[:240]}")
79
+ return _with_expiry(response.json())
80
+
81
+
82
+ def _with_expiry(payload: Mapping[str, Any]) -> dict[str, Any]:
83
+ result = dict(payload)
84
+ try:
85
+ result["expires_at"] = int(time.time()) + max(0, int(payload.get("expires_in", 0)))
86
+ except (TypeError, ValueError):
87
+ result["expires_at"] = 0
88
+ return result
89
+
90
+
91
+ def token_is_expiring(token: Mapping[str, Any], margin_seconds: int = 90) -> bool:
92
+ try:
93
+ return int(token.get("expires_at", 0)) <= int(time.time()) + margin_seconds
94
+ except (TypeError, ValueError):
95
+ return True
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from pathlib import Path
5
+ from typing import Any, Mapping
6
+
7
+ import requests
8
+
9
+ from .canva_auth import refresh_access_token, token_is_expiring
10
+
11
+ API_ROOT = "https://api.canva.com/rest/v1"
12
+
13
+
14
+ class CanvaClient:
15
+ def __init__(self, card: Mapping[str, Any], *, token_saver=None) -> None:
16
+ self.card = dict(card)
17
+ self.token_saver = token_saver
18
+ self.client_id = str(card.get("client_id") or "").strip()
19
+ self.client_secret = str(card.get("client_secret") or "").strip()
20
+ self.token = dict(card.get("oauth_token") or {})
21
+ self.base_url = str(card.get("base_url") or API_ROOT).rstrip("/")
22
+
23
+ def _save_token(self, token: Mapping[str, Any]) -> None:
24
+ self.token = dict(token)
25
+ if self.token_saver:
26
+ self.token_saver(self.token)
27
+
28
+ def _access_token(self) -> str:
29
+ if not self.token.get("access_token"):
30
+ raise RuntimeError("Canva não está autorizada. Autorize a integração em Configuração API > Imagem e Video IA.")
31
+ if token_is_expiring(self.token):
32
+ refresh = str(self.token.get("refresh_token") or "").strip()
33
+ if not refresh:
34
+ raise RuntimeError("O token Canva expirou e não existe refresh token. Autorize novamente.")
35
+ self._save_token(refresh_access_token(self.client_id, self.client_secret, refresh))
36
+ return str(self.token["access_token"])
37
+
38
+ def request(self, method: str, path: str, *, json: Mapping[str, Any] | None = None, timeout: int = 60) -> dict[str, Any]:
39
+ refreshed = False
40
+ for attempt in range(4):
41
+ try:
42
+ token = self._access_token()
43
+ response = requests.request(method, f"{self.base_url}/{path.lstrip('/')}", headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json", "Accept": "application/json"}, json=json, timeout=timeout)
44
+ except requests.RequestException as exc:
45
+ if attempt == 3:
46
+ raise RuntimeError(f"Falha de rede na Canva: {str(exc)[:200]}") from exc
47
+ time.sleep(2 ** attempt)
48
+ continue
49
+ if response.status_code == 401 and not refreshed and self.token.get("refresh_token"):
50
+ self._save_token(refresh_access_token(self.client_id, self.client_secret, str(self.token["refresh_token"])))
51
+ refreshed = True
52
+ continue
53
+ if response.status_code == 429:
54
+ if attempt == 3:
55
+ raise RuntimeError("Canva devolveu HTTP 429: limite de pedidos atingido.")
56
+ time.sleep(2 ** attempt)
57
+ continue
58
+ if response.status_code >= 400:
59
+ raise RuntimeError(f"Canva devolveu HTTP {response.status_code}: {response.text[:240]}")
60
+ return response.json()
61
+ raise RuntimeError("Canva não respondeu após as tentativas disponíveis.")
62
+
63
+ def create_design(self, width: int = 1280, height: int = 720, *, title: str = "Thunderbolt thumbnail") -> dict[str, Any]:
64
+ if not (40 <= width <= 8000 and 40 <= height <= 8000 and width * height <= 25_000_000):
65
+ raise ValueError("Dimensões Canva inválidas: cada dimensão deve estar entre 40 e 8000 e a área não pode exceder 25.000.000 px².")
66
+ return self.request("POST", "designs", json={"type": "type_and_asset", "design_type": {"type": "custom", "width": width, "height": height}, "title": title})
67
+
68
+ def export_design(self, design_id: str, *, file_type: str = "png", quality: str = "regular") -> str:
69
+ if file_type not in {"png", "jpg"}:
70
+ raise ValueError("A Canva thumbnail só suporta PNG ou JPG.")
71
+ if quality not in {"regular", "pro"}:
72
+ raise ValueError("A qualidade Canva deve ser regular ou pro.")
73
+ payload = self.request("POST", "exports", json={"design_id": design_id, "format": {"type": file_type, "export_quality": quality}})
74
+ job = payload.get("job") or payload
75
+ job_id = str(job.get("id") or "").strip()
76
+ if not job_id:
77
+ raise RuntimeError("Canva não devolveu o ID do job de exportação.")
78
+ return job_id
79
+
80
+ def get_export_job_status(self, job_id: str) -> dict[str, Any]:
81
+ return self.request("GET", f"exports/{job_id}", json=None)
82
+
83
+ def wait_export(self, job_id: str, *, attempts: int = 30, interval: float = 2.0) -> str:
84
+ for _ in range(attempts):
85
+ payload = self.get_export_job_status(job_id)
86
+ job = payload.get("job") or payload
87
+ status = str(job.get("status") or "").lower()
88
+ if status == "success" and job.get("urls"):
89
+ return str(job["urls"][0])
90
+ if status == "failed":
91
+ error = job.get("error") or {}
92
+ raise RuntimeError(f"Exportação Canva falhou: {error.get('code') or error}")
93
+ time.sleep(interval)
94
+ raise RuntimeError("A exportação Canva excedeu o tempo de espera.")
95
+
96
+ def download_export(self, url: str, destination: Path) -> Path:
97
+ try:
98
+ response = requests.get(url, timeout=90)
99
+ response.raise_for_status()
100
+ except requests.RequestException as exc:
101
+ raise RuntimeError(f"Não foi possível descarregar a thumbnail Canva: {str(exc)[:200]}") from exc
102
+ destination.parent.mkdir(parents=True, exist_ok=True)
103
+ destination.write_bytes(response.content)
104
+ return destination
105
+
106
+ def create_and_export_thumbnail(self, *, title: str, width: int = 1280, height: int = 720) -> Path:
107
+ design = self.create_design(width, height, title=title)
108
+ design_id = str((design.get("design") or design).get("id") or "")
109
+ if not design_id:
110
+ raise RuntimeError("Canva não devolveu o ID do design.")
111
+ job_id = self.export_design(design_id, file_type=str(self.card.get("export_format") or "png"), quality=str(self.card.get("export_quality") or "regular"))
112
+ url = self.wait_export(job_id)
113
+ destination = Path(str(self.card.get("output_path") or "thumbnail-canva.png"))
114
+ return self.download_export(url, destination)
@@ -38,8 +38,9 @@ from .provider_routing import (
38
38
  ProviderRoutingError,
39
39
  route_json_request,
40
40
  )
41
- from .storage import STORAGE, ensure_storage
41
+ from .storage import STORAGE, ensure_storage, write_json
42
42
  from .thumbnail_generation import _compose_thumbnail_prompt, generate_thumbnail_image, normalize_thumbnail_bytes
43
+ from .canva_client import CanvaClient
43
44
 
44
45
 
45
46
  class MediaGenerationError(RuntimeError):
@@ -327,6 +328,27 @@ def generate_image_for_card(
327
328
  """Generate one image with the selected media card."""
328
329
  card = dict(card)
329
330
  provider = str(card.get("provider") or "").strip().lower()
331
+ if provider == "canva":
332
+ ensure_storage()
333
+ export_format = str(card.get("export_format") or "png").lower()
334
+ destination = STORAGE / "thumbnails" / f"canva-{abs(hash((topic, prompt, variant_index))) & 0xffffffffffffffff:x}.{export_format if export_format in {'png', 'jpg'} else 'png'}"
335
+ card["output_path"] = str(destination)
336
+ def save_token(token: Mapping[str, Any]) -> None:
337
+ updated = dict(settings)
338
+ saved_cards = [dict(item) for item in updated.get("media_provider_cards", []) if isinstance(item, Mapping)]
339
+ for saved_card in saved_cards:
340
+ if str(saved_card.get("id")) == str(card.get("id")):
341
+ saved_card["oauth_token"] = dict(token)
342
+ updated["media_provider_cards"] = saved_cards
343
+ write_json("settings.json", updated)
344
+ try:
345
+ return CanvaClient(card, token_saver=save_token).create_and_export_thumbnail(
346
+ title=str(topic or "Thunderbolt thumbnail")[:255],
347
+ width=int(card.get("thumbnail_width") or 1280),
348
+ height=int(card.get("thumbnail_height") or 720),
349
+ )
350
+ except Exception as exc:
351
+ raise MediaGenerationError(str(exc)) from exc
330
352
  if provider == "nano_banana":
331
353
  merged = dict(settings)
332
354
  merged["gemini_image_api_key"] = _api_key(card)
@@ -394,9 +416,10 @@ def generate_image_from_pool(
394
416
  lettering_text: str = "",
395
417
  lettering_prompt: str = "",
396
418
  reference_image: Path | None = None,
419
+ thumbnail_only: bool = False,
397
420
  ) -> Path:
398
421
  """Try eligible image cards in priority order, without cross-pool fallback."""
399
- cards = media_cards_for_pool(settings, "image")
422
+ cards = media_cards_for_pool(settings, "image", thumbnail_only=True) if thumbnail_only else media_cards_for_pool(settings, "image")
400
423
  if not cards:
401
424
  raise MediaGenerationError("Não existem providers activos no pool de imagem.")
402
425
  errors: list[str] = []
@@ -55,6 +55,17 @@ MEDIA_PROVIDER_CATALOG: tuple[MediaProviderDefinition, ...] = (
55
55
  api_style="gemini_interactions",
56
56
  description="Geração de imagem nativa Gemini para thumbnails e artes.",
57
57
  ),
58
+ MediaProviderDefinition(
59
+ "canva",
60
+ "Canva Connect",
61
+ default_base_url="https://api.canva.com/rest/v1",
62
+ requires_api_key=False,
63
+ supports_image=True,
64
+ supports_video=False,
65
+ api_style="canva_connect",
66
+ extra_fields=("client_id", "client_secret", "redirect_uri", "export_quality", "export_format", "thumbnail_width", "thumbnail_height"),
67
+ description="Canva Connect para criação e exportação de thumbnails; não é usado no pool de vídeo.",
68
+ ),
58
69
  MediaProviderDefinition(
59
70
  "pollinations",
60
71
  "Pollinations.ai",
@@ -203,9 +214,12 @@ def normalize_media_card(card: Any, index: int = 0) -> dict[str, Any]:
203
214
  "supports_text": bool(source.get("supports_text", definition.supports_text)),
204
215
  "api_style": definition.api_style,
205
216
  "local": definition.local,
217
+ "thumbnail_only": provider == "canva",
206
218
  }
207
219
  for field in definition.extra_fields:
208
220
  result[field] = str(source.get(field) or "").strip()
221
+ if provider == "canva":
222
+ result["oauth_token"] = dict(source.get("oauth_token") or {}) if isinstance(source.get("oauth_token"), Mapping) else {}
209
223
  if provider == "nano_banana":
210
224
  result["aspect_ratio"] = str(source.get("aspect_ratio") or INTERNAL_IMAGE_ASPECT_RATIO).strip()
211
225
  result["image_size"] = str(source.get("image_size") or INTERNAL_IMAGE_SIZE).strip()
@@ -311,12 +325,14 @@ def apply_media_provider_cards_to_settings(settings: Mapping[str, Any], cards: l
311
325
  return result
312
326
 
313
327
 
314
- def media_cards_for_pool(settings: Mapping[str, Any], pool: str) -> list[dict[str, Any]]:
328
+ def media_cards_for_pool(settings: Mapping[str, Any], pool: str, *, thumbnail_only: bool = False) -> list[dict[str, Any]]:
315
329
  migrated, _ = ensure_media_provider_cards(settings)
316
330
  capability = "supports_image" if pool == "image" else "supports_video" if pool == "video" else "supports_text"
317
331
  active_key = MEDIA_IMAGE_ACTIVE_CARD_KEY if pool == "image" else MEDIA_VIDEO_ACTIVE_CARD_KEY if pool == "video" else ""
318
332
  active_id = str(migrated.get(active_key) or "")
319
333
  cards = [dict(item) for item in migrated.get(MEDIA_CARDS_KEY, []) if item.get("enabled", True) and item.get(capability)]
334
+ if pool == "image" and not thumbnail_only:
335
+ cards = [card for card in cards if not card.get("thumbnail_only")]
320
336
  # A prioridade é o contrato principal do failover. O cartão activo legado
321
337
  # só serve de desempate entre cartões com a mesma prioridade, para não
322
338
  # ignorar uma escolha explícita do utilizador por causa de estado antigo.
@@ -10,7 +10,7 @@ from typing import Any
10
10
  from .creative_generation import _language_instruction, generate_thumbnail_prompt
11
11
  from .media_generation import generate_image_from_pool
12
12
  from .media_providers import media_cards_for_pool
13
- from .storage import STORAGE, ensure_storage, now, read_json, update_json
13
+ from .storage import STORAGE, ensure_storage, now, read_json, update_json, write_json
14
14
  from .thumbnail_generation import ThumbnailGenerationError, generate_thumbnail_image
15
15
  from .thumbnail_blueprints import thumbnail_blueprint_for_channel
16
16
 
@@ -49,6 +49,7 @@ def _generate_image_with_pool(
49
49
  reference_image=reference_image,
50
50
  lettering_text=lettering_text,
51
51
  lettering_prompt=lettering_prompt,
52
+ thumbnail_only=True,
52
53
  )
53
54
 
54
55
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.4.91",
3
+ "version": "0.4.93",
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",
package/scripts/cli.mjs CHANGED
@@ -26,7 +26,7 @@ const venvPython = platform() === "win32" ? join(venvDir, "Scripts", "python.exe
26
26
  const python = process.env.THUNDERBOLT_PYTHON || process.env.HERMES_PYTHON || (existsSync(venvPython) ? venvPython : (platform() === "win32" ? "python" : "python3"));
27
27
  const main = resolve(root, "app", "main.py");
28
28
  const settingsPath = join(thunderboltHome, "storage", "state", "settings.json");
29
- const pythonEnvironment = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
29
+ const pythonEnvironment = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1", CLICK_NO_WIN_CONSOLE: "1" };
30
30
 
31
31
  function run(command, commandArgs, label = "comando", environment = process.env) {
32
32
  console.log(`Thunderbolt: a iniciar ${label}...`);
@@ -155,6 +155,7 @@ const runtimeEnv = {
155
155
  ...process.env,
156
156
  PYTHONIOENCODING: "utf-8",
157
157
  PYTHONUTF8: "1",
158
+ CLICK_NO_WIN_CONSOLE: "1",
158
159
  THUNDERBOLT_STORAGE_DIR: storageDir,
159
160
  STREAMLIT_BROWSER_GATHER_USAGE_STATS: "false",
160
161
  STREAMLIT_SERVER_HEADLESS: "true",
@@ -257,7 +258,7 @@ function startStreamlit() {
257
258
  child = spawn(python, ["-m", "streamlit", "run", main, "--server.port", String(backendPort), "--server.address", "127.0.0.1"], {
258
259
  cwd: root,
259
260
  stdio: "inherit",
260
- env: { ...runtimeEnv, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1", THUNDERBOLT_LAUNCHER_RESTART: "1" },
261
+ env: { ...runtimeEnv, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1", CLICK_NO_WIN_CONSOLE: "1", THUNDERBOLT_LAUNCHER_RESTART: "1" },
261
262
  windowsHide: false,
262
263
  });
263
264
  child.on("error", (error) => {
@@ -42,7 +42,7 @@ const defaultMpt = process.env.MONEYPRINTER_PATH || join(thunderboltHome, "Money
42
42
  const dependencyStatePath = join(thunderboltHome, "storage", "state", "install-state.json");
43
43
  const forceDeps = args.includes("--force-deps");
44
44
  const refreshMoneyPrinter = args.includes("--refresh-moneyprinter");
45
- const pythonEnvironment = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
45
+ const pythonEnvironment = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1", CLICK_NO_WIN_CONSOLE: "1" };
46
46
 
47
47
  function legacyRoots() {
48
48
  const userProfile = process.env.USERPROFILE || home;