@danhachuel/thunderbolt 0.5.60 → 0.5.62

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
@@ -1332,6 +1332,9 @@ def render_channel_edit_form(channel: dict, youtube_account_ids: list[str], yout
1332
1332
  edited_style = st.selectbox("Fonte do vídeo", WIDE_STYLE_OPTIONS, index=WIDE_STYLE_OPTIONS.index(channel_video_source_value(channel.get("style_wide"))) if channel_video_source_value(channel.get("style_wide")) in WIDE_STYLE_OPTIONS else 0)
1333
1333
  edited_aspect = st.selectbox("Proporção do vídeo", CHANNEL_ASPECT_RATIO_OPTIONS, index=CHANNEL_ASPECT_RATIO_OPTIONS.index(str(channel.get("video_aspect_ratio") or "Landscape 16:9")) if str(channel.get("video_aspect_ratio") or "Landscape 16:9") in CHANNEL_ASPECT_RATIO_OPTIONS else 0)
1334
1334
  edited_niche = st.text_input("Nicho", value=str(channel.get("niche") or channel_niche_label(channel) if channel_niche_label(channel) != "SEM NICHO CONFIGURADO" else "") )
1335
+ edited_subscribers = st.number_input("Inscritos", min_value=0, value=int(str(channel.get("subscriber_count") or 0).replace(".", "").replace(",", "")), step=1)
1336
+ edited_videos = st.number_input("Vídeos", min_value=0, value=int(str(channel.get("video_count") or 0).replace(".", "").replace(",", "")), step=1)
1337
+ edited_views = st.number_input("Visualizações", min_value=0, value=int(str(channel.get("view_count") or 0).replace(".", "").replace(",", "")), step=1)
1335
1338
  with edit_cols[1]:
1336
1339
  edited_blueprint = st.selectbox("Blueprint Padrão", blueprint_ids, index=blueprint_ids.index(current_blueprint) if current_blueprint in blueprint_ids else 0, format_func=lambda item: blueprint_labels.get(item, item or "Sem Blueprint padrão"))
1337
1340
  edited_voice = st.selectbox("Narrador/Voz Padrão", voice_options, index=voice_options.index(current_voice) if current_voice in voice_options else 0, format_func=lambda item: item or "Sem voz padrão")
@@ -1358,6 +1361,7 @@ def render_channel_edit_form(channel: dict, youtube_account_ids: list[str], yout
1358
1361
  "default_voice": edited_voice.strip(), "voice": edited_voice.strip(),
1359
1362
  "google_account_id": edited_account.strip(), "google_account_email": str(youtube_accounts_by_id.get(edited_account, {}).get("email", "")),
1360
1363
  "description": edited_description.strip(), "automation_on": bool(edited_automation), "automation_time": edited_time.strip(),
1364
+ "subscriber_count": int(edited_subscribers) or None, "video_count": int(edited_videos) or None, "view_count": int(edited_views) or None,
1361
1365
  })
1362
1366
  st.session_state.pop(f"edit_channel_{channel_id}", None)
1363
1367
  st.success("Canal actualizado.")
@@ -7281,6 +7285,9 @@ def _persist_media_cards(settings: dict[str, Any], cards: list[dict[str, Any]],
7281
7285
  def _fetch_media_models(card: dict[str, Any]) -> list[str]:
7282
7286
  """Consultar modelos do provider, incluindo o catálogo nativo Gemini."""
7283
7287
  provider = str(card.get("provider") or "").strip().lower()
7288
+ if provider == "huggingface":
7289
+ from integrations.openai_model_discovery import fetch_huggingface_text_to_image_models
7290
+ return fetch_huggingface_text_to_image_models(str(card.get("api_key") or ""))
7284
7291
  if provider == "replicate":
7285
7292
  from integrations.openai_model_discovery import fetch_replicate_models
7286
7293
  return fetch_replicate_models(str(card.get("api_key") or ""), str(card.get("base_url") or ""))
@@ -169,6 +169,14 @@ def test_media_provider_card(card: Mapping[str, Any]) -> dict[str, Any]:
169
169
  api_style = str(source.get("api_style") or "").strip().lower()
170
170
  if provider == "nano_banana":
171
171
  return test_nano_banana_credentials(api_key, model)
172
+ if provider == "huggingface":
173
+ if not api_key:
174
+ return _missing("Introduza o token Hugging Face antes de testar.")
175
+ return _get(
176
+ "https://huggingface.co/api/models",
177
+ headers={"Authorization": f"Bearer {api_key}"},
178
+ params={"pipeline_tag": "text-to-image", "limit": 1},
179
+ )
172
180
  if not base_url:
173
181
  return _result("missing", "Complete a Base URL antes de testar.")
174
182
  if provider not in {"inferenceport", "ollama", "lmstudio"} and not api_key:
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  import base64
11
11
  import binascii
12
+ import io
12
13
  import json
13
14
  import mimetypes
14
15
  import os
@@ -453,6 +454,35 @@ def generate_image_for_card(
453
454
  ensure_storage()
454
455
  destination = STORAGE / "thumbnails" / f"media-{provider}-{abs(hash((topic, prompt, variant_index))) & 0xffffffffffffffff:x}.jpg"
455
456
 
457
+ if provider == "huggingface":
458
+ model = _model(card)
459
+ token = _api_key(card)
460
+ if not model:
461
+ raise MediaGenerationError("Hugging Face requer a selecção de um modelo text-to-image.")
462
+ try:
463
+ image_prompt, _headline = _compose_thumbnail_prompt(
464
+ prompt,
465
+ topic=topic,
466
+ lettering_text=lettering_text,
467
+ lettering_prompt=lettering_prompt,
468
+ )
469
+ constrained_prompt = _append_generation_constraints(
470
+ image_prompt,
471
+ kind="image",
472
+ aspect_ratio="16:9",
473
+ size="1280x720 minimum",
474
+ )
475
+ from huggingface_hub import InferenceClient
476
+ image = InferenceClient(token=token or None, provider="hf-inference").text_to_image(
477
+ prompt=constrained_prompt,
478
+ model=model,
479
+ )
480
+ buffer = io.BytesIO()
481
+ image.save(buffer, format="PNG")
482
+ return _download_or_write(buffer.getvalue(), "", destination, card)
483
+ except Exception as exc:
484
+ raise MediaGenerationError(f"Hugging Face text-to-image falhou: {str(exc)[:240]}") from exc
485
+
456
486
  def request(current: dict[str, Any]) -> Any:
457
487
  return _image_request(current, prompt, topic=topic, lettering_text=lettering_text, lettering_prompt=lettering_prompt)
458
488
 
@@ -113,11 +113,11 @@ MEDIA_PROVIDER_CATALOG: tuple[MediaProviderDefinition, ...] = (
113
113
  MediaProviderDefinition(
114
114
  "huggingface",
115
115
  "Hugging Face Inference API",
116
- default_base_url="https://router.huggingface.co/v1",
116
+ default_base_url="https://huggingface.co",
117
117
  supports_image=True,
118
- supports_text=True,
118
+ supports_text=False,
119
119
  api_style="huggingface",
120
- description="Inference Providers; a capacidade depende do modelo seleccionado.",
120
+ description="Hugging Face Inference API para imagens via InferenceClient com provider hf-inference; não usa endpoint OpenAI-compatible.",
121
121
  ),
122
122
  MediaProviderDefinition(
123
123
  "cloudflare_workers_ai",
@@ -230,7 +230,7 @@ def normalize_media_card(card: Any, index: int = 0) -> dict[str, Any]:
230
230
  "provider": provider,
231
231
  "api_key": str(source.get("api_key") or source.get("key") or "").strip(),
232
232
  "model": str(source.get("model") or source.get("model_name") or "").strip(),
233
- "base_url": str(source.get("base_url") or "").strip() or definition.default_base_url,
233
+ "base_url": definition.default_base_url if provider == "huggingface" else (str(source.get("base_url") or "").strip() or definition.default_base_url),
234
234
  "enabled": bool(source.get("enabled", True)),
235
235
  "priority": max(0, int(source.get("priority", index)) if str(source.get("priority", index)).strip().lstrip("-").isdigit() else index),
236
236
  "supports_image": bool(source.get("supports_image", definition.supports_image)),
@@ -210,6 +210,40 @@ def fetch_openai_compatible_models(
210
210
  return normalize_model_ids(payload)
211
211
 
212
212
 
213
+ def fetch_huggingface_text_to_image_models(
214
+ api_key: str,
215
+ *,
216
+ timeout: float = DEFAULT_TIMEOUT_SECONDS,
217
+ limit: int = 2000,
218
+ ) -> list[str]:
219
+ """List only public Hugging Face models tagged with ``text-to-image``."""
220
+ token = str(api_key or "").strip()
221
+ headers = {"Accept": "application/json"}
222
+ if token:
223
+ headers["Authorization"] = f"Bearer {token}"
224
+ try:
225
+ response = requests.get(
226
+ "https://huggingface.co/api/models",
227
+ params={"pipeline_tag": "text-to-image", "limit": min(max(int(limit), 1), MAX_MODEL_IDS), "sort": "downloads", "direction": -1},
228
+ headers=headers,
229
+ timeout=timeout,
230
+ )
231
+ except requests.RequestException as exc:
232
+ raise ModelDiscoveryError(f"Não foi possível consultar o catálogo text-to-image da Hugging Face: {exc}") from exc
233
+ if response.status_code >= 400:
234
+ raise ModelDiscoveryError(f"O catálogo text-to-image da Hugging Face devolveu HTTP {response.status_code}.")
235
+ try:
236
+ payload = response.json()
237
+ except ValueError as exc:
238
+ raise ModelDiscoveryError("O catálogo Hugging Face devolveu uma resposta que não é JSON.") from exc
239
+ if not isinstance(payload, list):
240
+ raise ModelDiscoveryError("O catálogo Hugging Face não devolveu uma lista de modelos.")
241
+ models = sorted({str(item.get("id") or "").strip() for item in payload if isinstance(item, dict) and str(item.get("id") or "").strip()}, key=str.casefold)
242
+ if not models:
243
+ raise ModelDiscoveryError("A Hugging Face não devolveu modelos com pipeline text-to-image.")
244
+ return models[:MAX_MODEL_IDS]
245
+
246
+
213
247
  def fetch_replicate_models(
214
248
  api_key: str,
215
249
  base_url: str,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.5.60",
3
+ "version": "0.5.62",
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/requirements.txt CHANGED
@@ -1,6 +1,7 @@
1
1
  streamlit>=1.37,<2
2
2
  requests[socks]>=2.31,<3
3
3
  Pillow>=10,<12
4
+ huggingface_hub>=0.30,<1
4
5
  pandas>=2.0,<3
5
6
  openpyxl>=3.1,<4
6
7
  xlrd>=2.0,<3