@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.
Files changed (2) hide show
  1. package/app/main.py +147 -0
  2. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.3.29",
3
+ "version": "0.3.30",
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",