@danhachuel/thunderbolt 0.3.29 → 0.3.30
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 +147 -0
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -42,6 +42,7 @@ from hermes_ui.music import list_music_files, materialize_suno_audio, request_su
|
|
|
42
42
|
from hermes_ui.media_downloader import AUDIO_FORMATS, VIDEO_CONTAINERS, VIDEO_QUALITY_OPTIONS, MediaDownloadError, build_download_options, clear_media_download_history, dependency_status, download_media, list_media_downloads, media_download_file
|
|
43
43
|
from hermes_ui.notifications import clear_notifications, list_notifications, mark_all_notifications_read, mark_notification_read, notification_event_catalog, notification_preferences, record_notification, reconcile_persisted_notifications, save_notification_preferences, unread_notification_count
|
|
44
44
|
from hermes_ui.languages import LANGUAGE_CODES, LANGUAGE_FLAG_DATA_URIS, language_code, language_label, ui_language_menu_label, ui_text, video_language_label, video_language_options
|
|
45
|
+
from hermes_ui.api_key_tests import test_apify_credentials, test_kaggle_credentials, test_material_source_credentials, test_nano_banana_credentials, test_postiz_credentials, test_tiktok_credentials, test_upload_post_credentials, test_voice_provider
|
|
45
46
|
|
|
46
47
|
from hermes_ui.script_documents import list_script_documents, read_script_document, save_script_document, script_storage_path
|
|
47
48
|
from hermes_ui.script_generation import generate_script_document
|
|
@@ -4380,6 +4381,13 @@ def _render_material_source_card(settings: dict[str, Any], cards: list[dict[str,
|
|
|
4380
4381
|
value=active_card_id == card_id,
|
|
4381
4382
|
key=f"material_card_{card_id}_selected",
|
|
4382
4383
|
)
|
|
4384
|
+
if not is_local:
|
|
4385
|
+
_render_api_test_control(
|
|
4386
|
+
settings,
|
|
4387
|
+
f"material:{card_id}",
|
|
4388
|
+
lambda: test_material_source_credentials(provider, api_key),
|
|
4389
|
+
widget_key=f"api_test_material_{card_id}",
|
|
4390
|
+
)
|
|
4383
4391
|
save_card = st.form_submit_button("Salvar", type="primary", use_container_width=True, key=f"material_card_{card_id}_save")
|
|
4384
4392
|
if save_card:
|
|
4385
4393
|
cards[index] = {**card, "api_key": str(api_key or "").strip(), "enabled": bool(enabled)}
|
|
@@ -4438,6 +4446,61 @@ def _render_credential_status(value: Any, *, local: bool = False, required: bool
|
|
|
4438
4446
|
_api_status_badge(label, kind)
|
|
4439
4447
|
|
|
4440
4448
|
|
|
4449
|
+
def _persist_api_test_result(settings: dict[str, Any], test_key: str, result: dict[str, Any]) -> dict[str, Any]:
|
|
4450
|
+
"""Persist only the safe diagnosis fields; never store credentials or endpoint details."""
|
|
4451
|
+
safe_result = {
|
|
4452
|
+
"status": str(result.get("status") or "error"),
|
|
4453
|
+
"message": str(result.get("message") or "A chamada de diagnóstico falhou.")[:240],
|
|
4454
|
+
"status_code": result.get("status_code"),
|
|
4455
|
+
"checked_at": str(result.get("checked_at") or ""),
|
|
4456
|
+
}
|
|
4457
|
+
stored = settings.get("api_test_results")
|
|
4458
|
+
if not isinstance(stored, dict):
|
|
4459
|
+
stored = {}
|
|
4460
|
+
stored[str(test_key)] = safe_result
|
|
4461
|
+
settings["api_test_results"] = stored
|
|
4462
|
+
write_json("settings.json", settings)
|
|
4463
|
+
return safe_result
|
|
4464
|
+
|
|
4465
|
+
|
|
4466
|
+
def _render_api_test_feedback(settings: dict[str, Any], test_key: str, result: dict[str, Any] | None = None) -> None:
|
|
4467
|
+
"""Render the same compact green/red feedback for every non-LLM API card."""
|
|
4468
|
+
current = result
|
|
4469
|
+
if current is None:
|
|
4470
|
+
stored = settings.get("api_test_results")
|
|
4471
|
+
current = stored.get(test_key) if isinstance(stored, dict) else None
|
|
4472
|
+
if not isinstance(current, dict):
|
|
4473
|
+
return
|
|
4474
|
+
status = str(current.get("status") or "error")
|
|
4475
|
+
language = current_ui_language()
|
|
4476
|
+
if status == "success":
|
|
4477
|
+
st.success(ui_text("Último teste: API Key OK", language))
|
|
4478
|
+
return
|
|
4479
|
+
if status == "missing":
|
|
4480
|
+
st.error(ui_text("Último teste: falta configuração", language))
|
|
4481
|
+
return
|
|
4482
|
+
if status == "unsupported":
|
|
4483
|
+
st.warning(ui_text("Último teste: requer autorização ou endpoint seguro", language))
|
|
4484
|
+
return
|
|
4485
|
+
code = current.get("status_code")
|
|
4486
|
+
suffix = f" (HTTP {int(code)})" if isinstance(code, int) and code > 0 else ""
|
|
4487
|
+
st.error(ui_text("Último teste: chamada falhou", language) + suffix)
|
|
4488
|
+
|
|
4489
|
+
|
|
4490
|
+
def _render_api_test_control(settings: dict[str, Any], test_key: str, callback: Any, *, widget_key: str) -> None:
|
|
4491
|
+
"""Render a form-safe diagnostic button and persist its redacted result."""
|
|
4492
|
+
if st.form_submit_button("Testar chamada API", use_container_width=True, key=widget_key):
|
|
4493
|
+
with st.spinner(ui_text("A testar chamada API…", current_ui_language())):
|
|
4494
|
+
try:
|
|
4495
|
+
result = callback()
|
|
4496
|
+
except Exception:
|
|
4497
|
+
result = {"status": "error", "message": "A chamada de diagnóstico falhou."}
|
|
4498
|
+
_persist_api_test_result(settings, test_key, result)
|
|
4499
|
+
_render_api_test_feedback(settings, test_key, result)
|
|
4500
|
+
else:
|
|
4501
|
+
_render_api_test_feedback(settings, test_key)
|
|
4502
|
+
|
|
4503
|
+
|
|
4441
4504
|
def _llm_card_config_status(card: dict[str, Any]) -> tuple[str, str]:
|
|
4442
4505
|
definition = provider_definition(card.get("provider"))
|
|
4443
4506
|
if definition.local:
|
|
@@ -4649,6 +4712,12 @@ def render_settings():
|
|
|
4649
4712
|
with kaggle_cols[2]:
|
|
4650
4713
|
kaggle_kernel_slug = text_setting("Slug da kernel", "kaggle_kernel_slug", help_text="Identificador da kernel remota, por exemplo thunderbolt-niche-finder.")
|
|
4651
4714
|
_render_credential_status(kaggle_api_key)
|
|
4715
|
+
_render_api_test_control(
|
|
4716
|
+
settings,
|
|
4717
|
+
"kaggle",
|
|
4718
|
+
lambda: test_kaggle_credentials(kaggle_username, kaggle_api_key),
|
|
4719
|
+
widget_key="api_test_kaggle",
|
|
4720
|
+
)
|
|
4652
4721
|
|
|
4653
4722
|
with st.expander("Niche Finder — Apify", expanded=False):
|
|
4654
4723
|
st.caption("O token fica guardado apenas no storage local. A aba Niche Finder Apify só usa este serviço depois de clicar no botão de pesquisa.")
|
|
@@ -4662,6 +4731,12 @@ def render_settings():
|
|
|
4662
4731
|
with apify_cols[3]:
|
|
4663
4732
|
apify_run_timeout = st.number_input("Limite da execução (s)", min_value=30, max_value=7200, value=int(settings.get("apify_run_timeout_seconds", 900)), step=30)
|
|
4664
4733
|
_render_credential_status(apify_api_token)
|
|
4734
|
+
_render_api_test_control(
|
|
4735
|
+
settings,
|
|
4736
|
+
"apify",
|
|
4737
|
+
lambda: test_apify_credentials(apify_api_token),
|
|
4738
|
+
widget_key="api_test_apify",
|
|
4739
|
+
)
|
|
4665
4740
|
|
|
4666
4741
|
render_llm_provider_cards(settings, embedded=True)
|
|
4667
4742
|
|
|
@@ -4675,6 +4750,12 @@ def render_settings():
|
|
|
4675
4750
|
gemini_image_aspect_ratio = st.selectbox("Proporção da thumbnail", ["16:9", "9:16", "1:1", "4:5"], index=["16:9", "9:16", "1:1", "4:5"].index(str(settings.get("gemini_image_aspect_ratio") or "16:9")) if str(settings.get("gemini_image_aspect_ratio") or "16:9") in {"16:9", "9:16", "1:1", "4:5"} else 0)
|
|
4676
4751
|
gemini_image_size = st.selectbox("Tamanho da imagem", ["1K", "2K", "4K"], index=["1K", "2K", "4K"].index(str(settings.get("gemini_image_size") or "1K")) if str(settings.get("gemini_image_size") or "1K") in {"1K", "2K", "4K"} else 0)
|
|
4677
4752
|
_render_credential_status(gemini_image_api_key)
|
|
4753
|
+
_render_api_test_control(
|
|
4754
|
+
settings,
|
|
4755
|
+
"nano_banana",
|
|
4756
|
+
lambda: test_nano_banana_credentials(gemini_image_api_key, gemini_image_model),
|
|
4757
|
+
widget_key="api_test_nano_banana",
|
|
4758
|
+
)
|
|
4678
4759
|
|
|
4679
4760
|
|
|
4680
4761
|
with st.expander("Voz, TTS e música — Azure Speech, restantes serviços e Suno", expanded=False):
|
|
@@ -4707,11 +4788,65 @@ def render_settings():
|
|
|
4707
4788
|
suno_api_base_url = text_setting("Suno API Base URL", "suno_api_base_url", help_text="Use o endpoint compatível fornecido pelo seu acesso Suno; não é inventado pelo Thunderbolt.")
|
|
4708
4789
|
suno_api_endpoint = text_setting("Suno API endpoint", "suno_api_endpoint", help_text="Ex.: /api/generate")
|
|
4709
4790
|
|
|
4791
|
+
st.markdown(f"**{ui_text('Testar credenciais TTS e música', current_ui_language())}**")
|
|
4792
|
+
voice_test_cols = st.columns(3)
|
|
4793
|
+
with voice_test_cols[0]:
|
|
4794
|
+
_render_api_test_control(
|
|
4795
|
+
settings,
|
|
4796
|
+
"voice:azure_speech",
|
|
4797
|
+
lambda: test_voice_provider("azure_speech", {"azure_speech_key": azure_speech_key, "azure_speech_region": azure_speech_region}),
|
|
4798
|
+
widget_key="api_test_voice_azure",
|
|
4799
|
+
)
|
|
4800
|
+
_render_api_test_control(
|
|
4801
|
+
settings,
|
|
4802
|
+
"voice:siliconflow",
|
|
4803
|
+
lambda: test_voice_provider("siliconflow", {"siliconflow_tts_api_key": siliconflow_tts_api_key}),
|
|
4804
|
+
widget_key="api_test_voice_siliconflow",
|
|
4805
|
+
)
|
|
4806
|
+
_render_api_test_control(
|
|
4807
|
+
settings,
|
|
4808
|
+
"voice:minimax",
|
|
4809
|
+
lambda: test_voice_provider("minimax", {"minimax_tts_api_key": minimax_tts_api_key, "minimax_tts_base_url": minimax_tts_base_url}),
|
|
4810
|
+
widget_key="api_test_voice_minimax",
|
|
4811
|
+
)
|
|
4812
|
+
with voice_test_cols[1]:
|
|
4813
|
+
_render_api_test_control(
|
|
4814
|
+
settings,
|
|
4815
|
+
"voice:elevenlabs",
|
|
4816
|
+
lambda: test_voice_provider("elevenlabs", {"elevenlabs_api_key": elevenlabs_api_key}),
|
|
4817
|
+
widget_key="api_test_voice_elevenlabs",
|
|
4818
|
+
)
|
|
4819
|
+
_render_api_test_control(
|
|
4820
|
+
settings,
|
|
4821
|
+
"voice:chatterbox",
|
|
4822
|
+
lambda: test_voice_provider("chatterbox", {"chatterbox_api_key": chatterbox_api_key, "chatterbox_base_url": chatterbox_base_url}),
|
|
4823
|
+
widget_key="api_test_voice_chatterbox",
|
|
4824
|
+
)
|
|
4825
|
+
with voice_test_cols[2]:
|
|
4826
|
+
_render_api_test_control(
|
|
4827
|
+
settings,
|
|
4828
|
+
"voice:sonilo",
|
|
4829
|
+
lambda: test_voice_provider("sonilo", {"sonilo_api_key": sonilo_api_key, "sonilo_base_url": sonilo_base_url}),
|
|
4830
|
+
widget_key="api_test_voice_sonilo",
|
|
4831
|
+
)
|
|
4832
|
+
_render_api_test_control(
|
|
4833
|
+
settings,
|
|
4834
|
+
"voice:suno",
|
|
4835
|
+
lambda: test_voice_provider("suno", {"suno_api_key": suno_api_key, "suno_api_base_url": suno_api_base_url, "suno_api_endpoint": suno_api_endpoint}),
|
|
4836
|
+
widget_key="api_test_voice_suno",
|
|
4837
|
+
)
|
|
4838
|
+
|
|
4710
4839
|
with st.expander("TikTok for Developers — Client ID e Client Secret", expanded=False):
|
|
4711
4840
|
st.caption("Apenas as credenciais da aplicação ficam nesta UI. Redirect URI, scopes, autorização e tokens são geridos no TikTok for Developers Playground.")
|
|
4712
4841
|
tiktok_client_key = text_setting("TikTok Client ID", "tiktok_client_key", secret=True)
|
|
4713
4842
|
_render_credential_status(tiktok_client_key)
|
|
4714
4843
|
tiktok_client_secret = text_setting("TikTok Client Secret", "tiktok_client_secret", secret=True)
|
|
4844
|
+
_render_api_test_control(
|
|
4845
|
+
settings,
|
|
4846
|
+
"tiktok",
|
|
4847
|
+
lambda: test_tiktok_credentials(tiktok_client_key, tiktok_client_secret, settings.get("tiktok_access_token", "")),
|
|
4848
|
+
widget_key="api_test_tiktok",
|
|
4849
|
+
)
|
|
4715
4850
|
|
|
4716
4851
|
with st.expander("Publicação através do Upload-Post", expanded=False):
|
|
4717
4852
|
upload_post_enabled = st.checkbox("Activar Upload-Post", bool(settings.get("upload_post_enabled", False)))
|
|
@@ -4720,6 +4855,12 @@ def render_settings():
|
|
|
4720
4855
|
upload_post_username = text_setting("Upload-Post username", "upload_post_username")
|
|
4721
4856
|
upload_post_platforms = text_setting("Plataformas Upload-Post", "upload_post_platforms")
|
|
4722
4857
|
upload_post_auto_upload = st.checkbox("Publicar automaticamente após gerar", bool(settings.get("upload_post_auto_upload", False)))
|
|
4858
|
+
_render_api_test_control(
|
|
4859
|
+
settings,
|
|
4860
|
+
"upload_post",
|
|
4861
|
+
lambda: test_upload_post_credentials(upload_post_api_key, settings.get("upload_post_base_url", "https://api.upload-post.com/api")),
|
|
4862
|
+
widget_key="api_test_upload_post",
|
|
4863
|
+
)
|
|
4723
4864
|
|
|
4724
4865
|
with st.expander("Postiz — API key, integração e MCP", expanded=False):
|
|
4725
4866
|
st.caption("O Thunderbolt é o cliente. A API key é enviada exclusivamente ao servidor Postiz configurado; não é colocada em URLs, logs ou repositório.")
|
|
@@ -4735,6 +4876,12 @@ def render_settings():
|
|
|
4735
4876
|
postiz_mcp_url = text_setting("Postiz MCP URL", "postiz_mcp_url", help_text="Cloud: https://api.postiz.com/mcp · o cliente acrescenta a API key conforme o modo escolhido.")
|
|
4736
4877
|
postiz_auto_publish = st.checkbox("Permitir publicação imediata no Postiz", bool(settings.get("postiz_auto_publish", False)))
|
|
4737
4878
|
st.caption("No Upload, a aba Postiz permite carregar as integrações e enviar vídeos manualmente. No fluxo recomendado, Postiz só é tentado depois da API Oficial e do Upload directo.")
|
|
4879
|
+
_render_api_test_control(
|
|
4880
|
+
settings,
|
|
4881
|
+
"postiz",
|
|
4882
|
+
lambda: test_postiz_credentials(postiz_api_key, postiz_base_url),
|
|
4883
|
+
widget_key="api_test_postiz",
|
|
4884
|
+
)
|
|
4738
4885
|
|
|
4739
4886
|
save_all_settings = st.form_submit_button("Guardar configurações do Thunderbolt", type="primary")
|
|
4740
4887
|
if save_all_settings:
|
package/package.json
CHANGED