@danhachuel/thunderbolt 0.3.28 → 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 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:
@@ -0,0 +1,266 @@
1
+ """Safe, read-only API credential diagnostics used by the Settings UI.
2
+
3
+ Every check in this module is deliberately bounded and avoids uploads, posts,
4
+ actor runs, image generation and audio generation. Results contain no secret,
5
+ URL or raw exception text; callers may persist them in local settings safely.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from datetime import datetime, timezone
12
+ from typing import Any
13
+ from urllib.parse import quote, urlsplit, urlunsplit
14
+
15
+ import requests
16
+
17
+ from app.modules.niche_finder.apify import APIFY_API_BASE
18
+
19
+ DEFAULT_TIMEOUT = 20
20
+
21
+
22
+ def _result(status: str, message: str, *, status_code: int | None = None) -> dict[str, Any]:
23
+ """Build a small persistence-safe result object."""
24
+ return {
25
+ "ok": status == "success",
26
+ "status": status,
27
+ "message": message,
28
+ "status_code": status_code,
29
+ "checked_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
30
+ }
31
+
32
+
33
+ def _missing(message: str = "Introduza a credencial antes de testar.") -> dict[str, Any]:
34
+ return _result("missing", message)
35
+
36
+
37
+ def _unsupported(message: str) -> dict[str, Any]:
38
+ return _result("unsupported", message)
39
+
40
+
41
+ def _safe_url(value: str) -> str:
42
+ """Keep a configured base URL's scheme/host only; never echo credentials."""
43
+ try:
44
+ parsed = urlsplit(str(value or "").strip())
45
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
46
+ return ""
47
+ return urlunsplit((parsed.scheme, parsed.netloc, parsed.path.rstrip("/"), "", ""))
48
+ except ValueError:
49
+ return ""
50
+
51
+
52
+ def _response_result(response: Any) -> dict[str, Any]:
53
+ status_code = int(getattr(response, "status_code", 0) or 0)
54
+ if 200 <= status_code < 300:
55
+ return _result("success", "API Key OK", status_code=status_code)
56
+ if status_code in {401, 403}:
57
+ return _result("error", "A API rejeitou a credencial.", status_code=status_code)
58
+ if status_code == 404:
59
+ return _result("error", "O endpoint de diagnóstico não está disponível.", status_code=status_code)
60
+ if status_code == 429:
61
+ return _result("error", "A API limitou a chamada de diagnóstico.", status_code=status_code)
62
+ return _result("error", "A chamada de diagnóstico falhou.", status_code=status_code or None)
63
+
64
+
65
+ def _get(url: str, **kwargs: Any) -> dict[str, Any]:
66
+ try:
67
+ response = requests.get(url, timeout=DEFAULT_TIMEOUT, **kwargs)
68
+ except requests.RequestException:
69
+ return _result("error", "Não foi possível contactar o serviço.")
70
+ return _response_result(response)
71
+
72
+
73
+ def _post(url: str, **kwargs: Any) -> dict[str, Any]:
74
+ try:
75
+ response = requests.post(url, timeout=DEFAULT_TIMEOUT, **kwargs)
76
+ except requests.RequestException:
77
+ return _result("error", "Não foi possível contactar o serviço.")
78
+ return _response_result(response)
79
+
80
+
81
+ def test_kaggle_credentials(username: str, api_key: str) -> dict[str, Any]:
82
+ """Validate Kaggle Basic Auth with a read-only user lookup."""
83
+ username = str(username or "").strip()
84
+ api_key = str(api_key or "").strip()
85
+ if not username or not api_key:
86
+ return _missing("Introduza o username e a API key Kaggle antes de testar.")
87
+ return _get(f"https://www.kaggle.com/api/v1/users/list/{quote(username, safe='')}", auth=(username, api_key))
88
+
89
+
90
+ def test_apify_credentials(api_token: str) -> dict[str, Any]:
91
+ """Validate an Apify token without starting an Actor or reading a dataset."""
92
+ api_token = str(api_token or "").strip()
93
+ if not api_token:
94
+ return _missing()
95
+ return _get(f"{APIFY_API_BASE}/users/me", headers={"Authorization": f"Bearer {api_token}"})
96
+
97
+
98
+ def test_nano_banana_credentials(api_key: str, model: str) -> dict[str, Any]:
99
+ """Validate the configured Gemini image model metadata without generating an image."""
100
+ api_key = str(api_key or "").strip()
101
+ model = str(model or "").strip()
102
+ if not api_key:
103
+ return _missing()
104
+ if not model:
105
+ return _result("missing", "Complete a configuração do modelo antes de testar.")
106
+ model_path = model if model.startswith("models/") else f"models/{model}"
107
+ return _get(
108
+ f"https://generativelanguage.googleapis.com/v1beta/{model_path}",
109
+ headers={"x-goog-api-key": api_key},
110
+ )
111
+
112
+
113
+ def test_azure_speech_credentials(api_key: str, region: str) -> dict[str, Any]:
114
+ """Validate Azure Speech by listing voices; this does not synthesize audio."""
115
+ api_key = str(api_key or "").strip()
116
+ region = str(region or "").strip().lower()
117
+ if not api_key or not region:
118
+ return _missing("Introduza a Azure Speech key e a região antes de testar.")
119
+ if not region.replace("-", "").isalnum():
120
+ return _result("error", "A região Azure Speech não é válida.")
121
+ endpoint = f"https://{region}.tts.speech.microsoft.com/tts/cognitiveservices/voices/list"
122
+ return _get(endpoint, headers={"Ocp-Apim-Subscription-Key": api_key})
123
+
124
+
125
+ def test_elevenlabs_credentials(api_key: str) -> dict[str, Any]:
126
+ """Validate ElevenLabs with the documented read-only models endpoint."""
127
+ api_key = str(api_key or "").strip()
128
+ if not api_key:
129
+ return _missing()
130
+ return _get("https://api.elevenlabs.io/v1/models", headers={"xi-api-key": api_key})
131
+
132
+
133
+ def _models_endpoint(base_url: str) -> str:
134
+ base = _safe_url(base_url)
135
+ if not base:
136
+ return ""
137
+ return f"{base}/models" if base.endswith("/v1") else f"{base}/v1/models"
138
+
139
+
140
+ def test_siliconflow_credentials(api_key: str) -> dict[str, Any]:
141
+ """Validate SiliconFlow with its read-only model catalogue."""
142
+ api_key = str(api_key or "").strip()
143
+ if not api_key:
144
+ return _missing()
145
+ return _get("https://api.siliconflow.cn/v1/models", headers={"Authorization": f"Bearer {api_key}"})
146
+
147
+
148
+ def test_minimax_credentials(api_key: str, base_url: str) -> dict[str, Any]:
149
+ """Validate a configured MiniMax endpoint without calling text-to-audio."""
150
+ api_key = str(api_key or "").strip()
151
+ endpoint = _models_endpoint(base_url)
152
+ if not api_key or not str(base_url or "").strip():
153
+ return _missing("Introduza a MiniMax TTS key e a Base URL antes de testar.")
154
+ if not endpoint:
155
+ return _result("error", "A Base URL MiniMax não é válida.")
156
+ return _get(endpoint, headers={"Authorization": f"Bearer {api_key}"})
157
+
158
+
159
+ def test_openai_compatible_voice_credentials(provider: str, api_key: str, base_url: str) -> dict[str, Any]:
160
+ """Validate a local/custom voice service through a read-only models endpoint."""
161
+ provider = str(provider or "serviço").strip()
162
+ api_key = str(api_key or "").strip()
163
+ endpoint = _models_endpoint(base_url)
164
+ if not str(base_url or "").strip():
165
+ return _missing(f"Introduza a Base URL de {provider} antes de testar.")
166
+ if not endpoint:
167
+ return _result("error", f"A Base URL de {provider} não é válida.")
168
+ headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
169
+ return _get(endpoint, headers=headers)
170
+
171
+
172
+ def test_suno_credentials(api_key: str, base_url: str, endpoint: str) -> dict[str, Any]:
173
+ """Avoid a Suno generation call: custom deployments have no common safe health API."""
174
+ if not str(api_key or "").strip() or not str(base_url or "").strip():
175
+ return _missing("Introduza a Suno API key e a Base URL antes de testar.")
176
+ return _unsupported("Suno requer um endpoint de diagnóstico fornecido pelo serviço; o Thunderbolt não inicia uma geração só para testar.")
177
+
178
+
179
+ def test_tiktok_credentials(client_key: str, client_secret: str, access_token: str = "") -> dict[str, Any]:
180
+ """Validate TikTok only when an OAuth access token is available."""
181
+ client_key = str(client_key or "").strip()
182
+ client_secret = str(client_secret or "").strip()
183
+ token = str(access_token or os.getenv("TIKTOK_ACCESS_TOKEN", "") or "").strip()
184
+ if not client_key or not client_secret:
185
+ return _missing("Introduza o Client ID e o Client Secret TikTok antes de testar.")
186
+ if not token:
187
+ return _unsupported("TikTok só permite uma chamada autenticada depois da autorização OAuth; conclua o Playground e guarde um access token.")
188
+ return _get(
189
+ "https://open.tiktokapis.com/v2/user/info/?fields=open_id",
190
+ headers={"Authorization": f"Bearer {token}"},
191
+ )
192
+
193
+
194
+ def test_upload_post_credentials(api_key: str, base_url: str = "https://api.upload-post.com/api") -> dict[str, Any]:
195
+ """Validate Upload-Post through GET /uploadposts/me, never through an upload."""
196
+ api_key = str(api_key or "").strip()
197
+ base = _safe_url(base_url or "https://api.upload-post.com/api")
198
+ if not api_key:
199
+ return _missing()
200
+ if not base:
201
+ return _result("error", "A Base URL Upload-Post não é válida.")
202
+ return _get(f"{base}/uploadposts/me", headers={"Authorization": f"Apikey {api_key}"})
203
+
204
+
205
+ def test_postiz_credentials(api_key: str, base_url: str = "https://api.postiz.com/public/v1") -> dict[str, Any]:
206
+ """Validate Postiz with the read-only integrations endpoint."""
207
+ api_key = str(api_key or "").strip()
208
+ base = _safe_url(base_url or "https://api.postiz.com/public/v1")
209
+ if not api_key:
210
+ return _missing()
211
+ if not base:
212
+ return _result("error", "A Base URL Postiz não é válida.")
213
+ return _get(f"{base}/integrations", headers={"Authorization": api_key})
214
+
215
+
216
+ def test_material_source_credentials(provider: str, api_key: str) -> dict[str, Any]:
217
+ """Run a read-only provider-specific check for material source cards."""
218
+ provider = str(provider or "").strip().lower()
219
+ api_key = str(api_key or "").strip()
220
+ if not api_key:
221
+ return _missing()
222
+ if provider == "pexels":
223
+ return _get("https://api.pexels.com/v1/curated", params={"per_page": 1}, headers={"Authorization": api_key})
224
+ if provider == "pixabay":
225
+ return _get("https://pixabay.com/api/", params={"key": api_key, "per_page": 3})
226
+ if provider == "coverr":
227
+ return _get("https://api.coverr.co/v1/videos", headers={"Authorization": f"Bearer {api_key}"})
228
+ return _unsupported("Este provider de materiais não expõe um endpoint de diagnóstico seguro no cartão actual.")
229
+
230
+
231
+ def test_voice_provider(provider: str, settings: dict[str, Any]) -> dict[str, Any]:
232
+ """Dispatch a non-generating check for one provider in the voice/music group."""
233
+ provider = str(provider or "").strip().lower()
234
+ if provider == "azure_speech":
235
+ return test_azure_speech_credentials(settings.get("azure_speech_key", ""), settings.get("azure_speech_region", ""))
236
+ if provider == "elevenlabs":
237
+ return test_elevenlabs_credentials(settings.get("elevenlabs_api_key", ""))
238
+ if provider == "siliconflow":
239
+ return test_siliconflow_credentials(settings.get("siliconflow_tts_api_key", ""))
240
+ if provider == "minimax":
241
+ return test_minimax_credentials(settings.get("minimax_tts_api_key", ""), settings.get("minimax_tts_base_url", ""))
242
+ if provider == "chatterbox":
243
+ return test_openai_compatible_voice_credentials("Chatterbox", settings.get("chatterbox_api_key", ""), settings.get("chatterbox_base_url", ""))
244
+ if provider == "sonilo":
245
+ return test_openai_compatible_voice_credentials("Sonilo", settings.get("sonilo_api_key", ""), settings.get("sonilo_base_url", ""))
246
+ if provider == "suno":
247
+ return test_suno_credentials(settings.get("suno_api_key", ""), settings.get("suno_api_base_url", ""), settings.get("suno_api_endpoint", ""))
248
+ return _unsupported("Este provider de voz não tem diagnóstico remoto configurado.")
249
+
250
+
251
+ __all__ = [
252
+ "test_apify_credentials",
253
+ "test_azure_speech_credentials",
254
+ "test_elevenlabs_credentials",
255
+ "test_kaggle_credentials",
256
+ "test_material_source_credentials",
257
+ "test_minimax_credentials",
258
+ "test_nano_banana_credentials",
259
+ "test_openai_compatible_voice_credentials",
260
+ "test_postiz_credentials",
261
+ "test_siliconflow_credentials",
262
+ "test_suno_credentials",
263
+ "test_tiktok_credentials",
264
+ "test_upload_post_credentials",
265
+ "test_voice_provider",
266
+ ]
@@ -1222,3 +1222,91 @@ _MATERIAL_SOURCE_TRANSLATIONS: dict[str, dict[str, str]] = {
1222
1222
  }
1223
1223
  for _language_code, _material_source_translation in _MATERIAL_SOURCE_TRANSLATIONS.items():
1224
1224
  UI_TRANSLATIONS[_language_code].update(_material_source_translation)
1225
+
1226
+
1227
+ # API credential diagnostics. Keep these keys stable because the settings
1228
+ # renderer calls ui_text() for every visible result in all supported languages.
1229
+ _API_TEST_TRANSLATIONS: dict[str, dict[str, str]] = {
1230
+ "pt": {
1231
+ "A testar chamada API…": "A testar chamada API…",
1232
+ "Último teste: API Key OK": "Último teste: API Key OK",
1233
+ "Último teste: falta configuração": "Último teste: falta configuração",
1234
+ "Último teste: requer autorização ou endpoint seguro": "Último teste: requer autorização ou endpoint seguro",
1235
+ "Último teste: chamada falhou": "Último teste: chamada falhou",
1236
+ "Testar credenciais TTS e música": "Testar credenciais TTS e música",
1237
+ },
1238
+ "en": {
1239
+ "A testar chamada API…": "Testing API call…",
1240
+ "Último teste: API Key OK": "Last test: API key OK",
1241
+ "Último teste: falta configuração": "Last test: configuration missing",
1242
+ "Último teste: requer autorização ou endpoint seguro": "Last test: additional authorization or a safe endpoint is required",
1243
+ "Último teste: chamada falhou": "Last test: call failed",
1244
+ "Testar credenciais TTS e música": "Test TTS and music credentials",
1245
+ },
1246
+ "zh": {
1247
+ "A testar chamada API…": "正在测试 API 调用…",
1248
+ "Último teste: API Key OK": "上次测试:API 密钥正常",
1249
+ "Último teste: falta configuração": "上次测试:缺少配置",
1250
+ "Último teste: requer autorização ou endpoint seguro": "上次测试:需要额外授权或安全端点",
1251
+ "Último teste: chamada falhou": "上次测试:调用失败",
1252
+ "Testar credenciais TTS e música": "测试 TTS 和音乐凭证",
1253
+ },
1254
+ "de": {
1255
+ "A testar chamada API…": "API-Aufruf wird getestet…",
1256
+ "Último teste: API Key OK": "Letzter Test: API-Schlüssel OK",
1257
+ "Último teste: falta configuração": "Letzter Test: Konfiguration fehlt",
1258
+ "Último teste: requer autorização ou endpoint seguro": "Letzter Test: zusätzliche Autorisierung oder ein sicherer Endpunkt erforderlich",
1259
+ "Último teste: chamada falhou": "Letzter Test: Aufruf fehlgeschlagen",
1260
+ "Testar credenciais TTS e música": "TTS- und Musik-Anmeldedaten testen",
1261
+ },
1262
+ "vi": {
1263
+ "A testar chamada API…": "Đang kiểm tra lệnh gọi API…",
1264
+ "Último teste: API Key OK": "Lần kiểm tra cuối: Khóa API hợp lệ",
1265
+ "Último teste: falta configuração": "Lần kiểm tra cuối: Thiếu cấu hình",
1266
+ "Último teste: requer autorização ou endpoint seguro": "Lần kiểm tra cuối: Cần ủy quyền bổ sung hoặc endpoint an toàn",
1267
+ "Último teste: chamada falhou": "Lần kiểm tra cuối: Lệnh gọi thất bại",
1268
+ "Testar credenciais TTS e música": "Kiểm tra thông tin xác thực TTS và âm nhạc",
1269
+ },
1270
+ "tr": {
1271
+ "A testar chamada API…": "API çağrısı test ediliyor…",
1272
+ "Último teste: API Key OK": "Son test: API anahtarı OK",
1273
+ "Último teste: falta configuração": "Son test: yapılandırma eksik",
1274
+ "Último teste: requer autorização ou endpoint seguro": "Son test: ek yetkilendirme veya güvenli uç nokta gerekli",
1275
+ "Último teste: chamada falhou": "Son test: çağrı başarısız",
1276
+ "Testar credenciais TTS e música": "TTS ve müzik kimlik bilgilerini test et",
1277
+ },
1278
+ "ru": {
1279
+ "A testar chamada API…": "Проверка вызова API…",
1280
+ "Último teste: API Key OK": "Последняя проверка: API-ключ действителен",
1281
+ "Último teste: falta configuração": "Последняя проверка: не хватает конфигурации",
1282
+ "Último teste: requer autorização ou endpoint seguro": "Последняя проверка: требуется дополнительная авторизация или безопасный эндпоинт",
1283
+ "Último teste: chamada falhou": "Последняя проверка: вызов не выполнен",
1284
+ "Testar credenciais TTS e música": "Проверить учётные данные TTS и музыки",
1285
+ },
1286
+ "es": {
1287
+ "A testar chamada API…": "Probando la llamada API…",
1288
+ "Último teste: API Key OK": "Última prueba: clave API correcta",
1289
+ "Último teste: falta configuração": "Última prueba: falta configuración",
1290
+ "Último teste: requer autorização ou endpoint seguro": "Última prueba: se requiere autorización adicional o un endpoint seguro",
1291
+ "Último teste: chamada falhou": "Última prueba: la llamada falló",
1292
+ "Testar credenciais TTS e música": "Probar credenciales de TTS y música",
1293
+ },
1294
+ "id": {
1295
+ "A testar chamada API…": "Menguji panggilan API…",
1296
+ "Último teste: API Key OK": "Tes terakhir: Kunci API OK",
1297
+ "Último teste: falta configuração": "Tes terakhir: konfigurasi tidak lengkap",
1298
+ "Último teste: requer autorização ou endpoint seguro": "Tes terakhir: diperlukan otorisasi tambahan atau endpoint aman",
1299
+ "Último teste: chamada falhou": "Tes terakhir: panggilan gagal",
1300
+ "Testar credenciais TTS e música": "Uji kredensial TTS dan musik",
1301
+ },
1302
+ "it": {
1303
+ "A testar chamada API…": "Test della chiamata API in corso…",
1304
+ "Último teste: API Key OK": "Ultimo test: chiave API OK",
1305
+ "Último teste: falta configuração": "Ultimo test: configurazione mancante",
1306
+ "Último teste: requer autorização ou endpoint seguro": "Ultimo test: è necessaria un'autorizzazione aggiuntiva o un endpoint sicuro",
1307
+ "Último teste: chamada falhou": "Ultimo test: chiamata non riuscita",
1308
+ "Testar credenciais TTS e música": "Testa le credenziali TTS e musica",
1309
+ },
1310
+ }
1311
+ for _language_code, _api_test_values in _API_TEST_TRANSLATIONS.items():
1312
+ UI_TRANSLATIONS[_language_code].update(_api_test_values)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.3.28",
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",