@danhachuel/thunderbolt 0.5.59 → 0.5.61
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
|
@@ -6729,7 +6729,11 @@ def render_google_accounts(*, include_innertube: bool = True):
|
|
|
6729
6729
|
|
|
6730
6730
|
st.divider()
|
|
6731
6731
|
add_account = False
|
|
6732
|
-
|
|
6732
|
+
add_form_open = bool(st.session_state.get("add_batch_account_form_open", False))
|
|
6733
|
+
if not add_form_open and st.button("Adicionar conta Google/YouTube", type="primary", use_container_width=True, key="open_add_batch_account_form"):
|
|
6734
|
+
st.session_state["add_batch_account_form_open"] = True
|
|
6735
|
+
st.rerun()
|
|
6736
|
+
if add_form_open:
|
|
6733
6737
|
st.markdown("### Nova conta Gmail")
|
|
6734
6738
|
st.caption("Este formulário fica fora dos cartões das contas existentes. A associação de canais não depende da completude deste documento; ela apenas ficará pendente para Upload directo até os campos serem preenchidos.")
|
|
6735
6739
|
with st.form("add_batch_account_form"):
|
|
@@ -6742,7 +6746,10 @@ def render_google_accounts(*, include_innertube: bool = True):
|
|
|
6742
6746
|
new_account_client_secret = st.text_input("OAuth Client Secret", type="password", key="new_batch_account_client_secret")
|
|
6743
6747
|
new_account_session_info = st.text_input("sessionInfo token desta conta Google", type="password", key="new_batch_account_session_info", help="Token sessionInfo desta conta. Os cookies e delegated_session_ids ficam no documento; a INNERTUBE_API_KEY é configurada no bloco próprio acima.")
|
|
6744
6748
|
new_account_document = st.file_uploader("Documento de cookies/credenciais opcional", type=["json"], key="new_batch_account_credentials_document", help="Pode subir agora um JSON completo ou apenas o documento de cookies. Se não subir, será criado um credentials.json padrão vazio.")
|
|
6745
|
-
add_account = st.form_submit_button("
|
|
6749
|
+
add_account = st.form_submit_button("Guardar conta Google/YouTube", type="primary", use_container_width=True)
|
|
6750
|
+
if st.button("Cancelar", key="cancel_add_batch_account_form"):
|
|
6751
|
+
st.session_state["add_batch_account_form_open"] = False
|
|
6752
|
+
st.rerun()
|
|
6746
6753
|
if add_account:
|
|
6747
6754
|
document_error = ""
|
|
6748
6755
|
if "@" not in new_account_email.strip():
|
|
@@ -6772,6 +6779,7 @@ def render_google_accounts(*, include_innertube: bool = True):
|
|
|
6772
6779
|
st.success(f"Conta {new_account['email']} adicionada e documento incorporado.")
|
|
6773
6780
|
else:
|
|
6774
6781
|
st.warning(f"Conta {new_account['email']} adicionada. Foi criado um credentials.json padrão; suba o documento de cookies nesta conta quando estiver pronto.")
|
|
6782
|
+
st.session_state["add_batch_account_form_open"] = False
|
|
6775
6783
|
st.rerun()
|
|
6776
6784
|
|
|
6777
6785
|
st.divider()
|
|
@@ -7273,6 +7281,9 @@ def _persist_media_cards(settings: dict[str, Any], cards: list[dict[str, Any]],
|
|
|
7273
7281
|
def _fetch_media_models(card: dict[str, Any]) -> list[str]:
|
|
7274
7282
|
"""Consultar modelos do provider, incluindo o catálogo nativo Gemini."""
|
|
7275
7283
|
provider = str(card.get("provider") or "").strip().lower()
|
|
7284
|
+
if provider == "huggingface":
|
|
7285
|
+
from integrations.openai_model_discovery import fetch_huggingface_text_to_image_models
|
|
7286
|
+
return fetch_huggingface_text_to_image_models(str(card.get("api_key") or ""))
|
|
7276
7287
|
if provider == "replicate":
|
|
7277
7288
|
from integrations.openai_model_discovery import fetch_replicate_models
|
|
7278
7289
|
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://
|
|
116
|
+
default_base_url="https://huggingface.co",
|
|
117
117
|
supports_image=True,
|
|
118
|
-
supports_text=
|
|
118
|
+
supports_text=False,
|
|
119
119
|
api_style="huggingface",
|
|
120
|
-
description="Inference
|
|
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