@danhachuel/thunderbolt 0.3.16 → 0.3.18

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
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import hashlib
4
4
  import json
5
5
  import re
6
+ from contextlib import nullcontext
6
7
  from datetime import date, datetime
7
8
  import sys
8
9
  import uuid
@@ -3752,7 +3753,7 @@ def _persist_llm_cards(settings: dict[str, Any], cards: list[dict[str, Any]], ac
3752
3753
  return updated
3753
3754
 
3754
3755
 
3755
- def _render_llm_card(settings: dict[str, Any], cards: list[dict[str, Any]], index: int) -> None:
3756
+ def _render_llm_card(settings: dict[str, Any], cards: list[dict[str, Any]], index: int, *, embedded: bool = False) -> None:
3756
3757
  card = normalize_llm_card(cards[index], index)
3757
3758
  cards[index] = card
3758
3759
  card_id = str(card["id"])
@@ -3766,7 +3767,8 @@ def _render_llm_card(settings: dict[str, Any], cards: list[dict[str, Any]], inde
3766
3767
  with header_cols[1]:
3767
3768
  status_kind, status_label = _llm_card_config_status(card)
3768
3769
  _api_status_badge(status_label, status_kind)
3769
- with st.form(f"llm_card_form_{card_id}"):
3770
+ card_form = nullcontext() if embedded else st.form(f"llm_card_form_{card_id}")
3771
+ with card_form:
3770
3772
  key_col, model_col = st.columns(2)
3771
3773
  with key_col:
3772
3774
  api_key = card.get("api_key", "")
@@ -3806,9 +3808,9 @@ def _render_llm_card(settings: dict[str, Any], cards: list[dict[str, Any]], inde
3806
3808
  with action_col:
3807
3809
  action_buttons = st.columns(2)
3808
3810
  with action_buttons[0]:
3809
- refresh_clicked = st.form_submit_button("Consultar modelos", use_container_width=True)
3811
+ refresh_clicked = st.form_submit_button("Consultar modelos", use_container_width=True, key=f"llm_card_{card_id}_refresh")
3810
3812
  with action_buttons[1]:
3811
- test_clicked = st.form_submit_button("Testar chamada API", use_container_width=True)
3813
+ test_clicked = st.form_submit_button("Testar chamada API", use_container_width=True, key=f"llm_card_{card_id}_test")
3812
3814
 
3813
3815
  extra_values: dict[str, str] = {}
3814
3816
  if definition.extra_fields:
@@ -3839,10 +3841,10 @@ def _render_llm_card(settings: dict[str, Any], cards: list[dict[str, Any]], inde
3839
3841
  key=f"llm_card_{card_id}_telegram",
3840
3842
  )
3841
3843
 
3842
- save_clicked = st.form_submit_button("Salvar", type="primary", use_container_width=True)
3844
+ save_clicked = st.form_submit_button("Salvar", type="primary", use_container_width=True, key=f"llm_card_{card_id}_save")
3843
3845
  remove_clicked = False
3844
3846
  if definition.code != "openai":
3845
- remove_clicked = st.form_submit_button("Remover cartão", use_container_width=True)
3847
+ remove_clicked = st.form_submit_button("Remover cartão", use_container_width=True, key=f"llm_card_{card_id}_remove")
3846
3848
 
3847
3849
  edited = dict(card)
3848
3850
  edited.update({"api_key": str(api_key or "").strip(), "model": str(model or "").strip(), "base_url": str(base_url or "").strip(), "enabled": bool(enabled), "telegram_llm": bool(telegram_llm), **extra_values})
@@ -3883,7 +3885,7 @@ def _render_llm_card(settings: dict[str, Any], cards: list[dict[str, Any]], inde
3883
3885
  st.error(f"Último teste: {saved_test['message']}")
3884
3886
 
3885
3887
 
3886
- def render_llm_provider_cards(settings: dict[str, Any]) -> None:
3888
+ def render_llm_provider_cards(settings: dict[str, Any], *, embedded: bool = False) -> None:
3887
3889
  """Renderizar cartões LLM fora da form global para permitir acções por cartão."""
3888
3890
  migrated, changed = ensure_llm_provider_cards(settings)
3889
3891
  cards = [dict(item) for item in migrated.get(LLM_CARDS_KEY, [])]
@@ -3893,7 +3895,7 @@ def render_llm_provider_cards(settings: dict[str, Any]) -> None:
3893
3895
  with st.expander("LLM — providers e modelos", expanded=False):
3894
3896
  st.caption("Configure cada provider num cartão independente. Pode repetir o mesmo provider para manter várias API keys; o cartão activo é usado pela geração de conteúdo.")
3895
3897
  for index in range(len(cards)):
3896
- _render_llm_card(settings, cards, index)
3898
+ _render_llm_card(settings, cards, index, embedded=embedded)
3897
3899
  st.divider()
3898
3900
  st.markdown("**Adicionar provider LLM**")
3899
3901
  provider_codes = [item.code for item in LLM_PROVIDER_CATALOG]
@@ -3903,7 +3905,12 @@ def render_llm_provider_cards(settings: dict[str, Any]) -> None:
3903
3905
  format_func=lambda value: ui_text(provider_definition(value).label, current_ui_language()),
3904
3906
  key="llm_new_provider_choice",
3905
3907
  )
3906
- if st.button("Configurar Novo Provedor LLM", type="primary", use_container_width=True, key="add_llm_provider_card"):
3908
+ add_provider_clicked = (
3909
+ st.form_submit_button("Configurar Novo Provedor LLM", type="primary", use_container_width=True, key="add_llm_provider_card")
3910
+ if embedded
3911
+ else st.button("Configurar Novo Provedor LLM", type="primary", use_container_width=True, key="add_llm_provider_card")
3912
+ )
3913
+ if add_provider_clicked:
3907
3914
  new_card = new_llm_card(provider_to_add, card_id=f"llm-{provider_to_add}-{uuid.uuid4().hex[:8]}")
3908
3915
  cards.append(new_card)
3909
3916
  _persist_llm_cards(settings, cards, str(settings.get(LLM_ACTIVE_CARD_KEY) or DEFAULT_LLM_CARD_ID))
@@ -3924,147 +3931,146 @@ def render_settings():
3924
3931
  key=f"settings_{key}",
3925
3932
  )
3926
3933
 
3927
- api_keys_tab, voice_test_tab = render_localized_tabs(["API Keys", "Teste de vozes"])
3934
+ api_keys_tab, material_sources_tab, voice_test_tab = render_localized_tabs(["API Keys", "Fontes de Materiais", "Teste de Voz"])
3928
3935
 
3929
3936
  with api_keys_tab:
3930
- api_service_tab, material_sources_tab = render_localized_tabs(["Serviços e modelos", "Fontes de materiais"])
3931
- with api_service_tab:
3932
- with st.container(border=True):
3933
- st.subheader("API Keys")
3934
- port = st.number_input("Porta Streamlit", 1, 65535, int(settings.get("port", 3030)))
3935
- moneyprinter_path = st.text_input("Pasta do motor de vídeo", settings.get("moneyprinter_path", ""), key="settings_moneyprinter_path")
3936
- render_llm_provider_cards(settings)
3937
- with st.form("settings_form"):
3938
- with st.expander("Niche Finder — Kaggle", expanded=False):
3939
- st.caption("O dataset permanece no Kaggle. O Thunderbolt usa estas credenciais apenas para publicar/executar a kernel e obter os resultados pequenos da análise.")
3940
- kaggle_cols = st.columns(3)
3941
- with kaggle_cols[0]:
3942
- kaggle_username = text_setting("Kaggle Username", "kaggle_username", help_text="Nome de utilizador da sua conta Kaggle, sem @ e sem URL.")
3943
- with kaggle_cols[1]:
3944
- kaggle_api_key = text_setting("Kaggle API Key", "kaggle_api_key", secret=True, help_text="Chave criada em Kaggle > Settings > API. Nunca é incluída no notebook ou no GitHub.")
3945
- with kaggle_cols[2]:
3946
- kaggle_kernel_slug = text_setting("Slug da kernel", "kaggle_kernel_slug", help_text="Identificador da kernel remota, por exemplo thunderbolt-niche-finder.")
3947
- _render_credential_status(kaggle_api_key)
3948
-
3949
- with st.expander("Niche Finder — Apify", expanded=False):
3950
- st.caption("O token fica guardado apenas no storage local. A aba Niche Finder Apify usa este serviço depois de clicar no botão de pesquisa.")
3951
- apify_cols = st.columns(4)
3952
- with apify_cols[0]:
3953
- apify_api_token = text_setting("Apify API Token", "apify_api_token", secret=True, help_text="Token pessoal da Apify. Não é incluído no workflow, logs ou GitHub.")
3954
- with apify_cols[1]:
3955
- apify_actor_id = text_setting("Apify Actor ID", "apify_actor_id", help_text="Por padrão: streamers~youtube-scraper.")
3956
- with apify_cols[2]:
3957
- apify_poll_interval = st.number_input("Intervalo de consulta (s)", min_value=1, max_value=120, value=int(settings.get("apify_poll_interval_seconds", 10)), step=1)
3958
- with apify_cols[3]:
3959
- 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)
3960
- _render_credential_status(apify_api_token)
3961
-
3962
- with st.expander("Nano Banana geração de thumbnails", expanded=False):
3963
- st.caption("A Nano Banana gera a imagem final das thumbnails a partir da variante escolhida. A chave é guardada apenas no storage local e é distinta da chave do Gemini usado como LLM textual.")
3964
- nano_cols = st.columns(2)
3965
- with nano_cols[0]:
3966
- gemini_image_api_key = text_setting("Nano Banana API key", "gemini_image_api_key", secret=True, help_text="Chave criada no Google AI Studio para a API Gemini. Nunca é incluída no código, logs ou pacote.")
3967
- gemini_image_model = st.selectbox("Modelo Nano Banana", ["gemini-3.1-flash-image", "gemini-3-pro-image", "gemini-2.5-flash-image"], index=["gemini-3.1-flash-image", "gemini-3-pro-image", "gemini-2.5-flash-image"].index(str(settings.get("gemini_image_model") or "gemini-3.1-flash-image")) if str(settings.get("gemini_image_model") or "gemini-3.1-flash-image") in {"gemini-3.1-flash-image", "gemini-3-pro-image", "gemini-2.5-flash-image"} else 0)
3968
- with nano_cols[1]:
3969
- 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)
3970
- 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)
3971
- _render_credential_status(gemini_image_api_key)
3972
-
3973
-
3974
- with st.expander("Voz, TTS e música — Azure Speech, restantes serviços e Suno", expanded=False):
3975
- cols = st.columns(2)
3976
- with cols[0]:
3977
- azure_speech_key = text_setting("Azure Speech key", "azure_speech_key", secret=True)
3978
- _render_credential_status(azure_speech_key)
3979
- azure_speech_region = text_setting("Azure Speech region", "azure_speech_region")
3980
- siliconflow_tts_api_key = text_setting("SiliconFlow TTS API key", "siliconflow_tts_api_key", secret=True)
3981
- _render_credential_status(siliconflow_tts_api_key)
3982
- minimax_tts_api_key = text_setting("MiniMax TTS API key", "minimax_tts_api_key", secret=True)
3983
- _render_credential_status(minimax_tts_api_key)
3984
- minimax_tts_base_url = text_setting("MiniMax TTS Base URL", "minimax_tts_base_url")
3985
- minimax_tts_model_id = text_setting("MiniMax TTS model", "minimax_tts_model_id")
3986
- minimax_tts_voice_id = text_setting("MiniMax TTS voice ID", "minimax_tts_voice_id")
3987
- with cols[1]:
3988
- elevenlabs_api_key = text_setting("ElevenLabs API key", "elevenlabs_api_key", secret=True)
3989
- _render_credential_status(elevenlabs_api_key)
3990
- elevenlabs_model_id = text_setting("ElevenLabs model", "elevenlabs_model_id")
3991
- chatterbox_base_url = text_setting("Chatterbox Base URL", "chatterbox_base_url")
3992
- chatterbox_api_key = text_setting("Chatterbox API key", "chatterbox_api_key", secret=True)
3993
- _render_credential_status(chatterbox_api_key, local=True, required=False)
3994
- chatterbox_model_id = text_setting("Chatterbox model", "chatterbox_model_id")
3995
- sonilo_api_key = text_setting("Sonilo API key", "sonilo_api_key", secret=True)
3996
- _render_credential_status(sonilo_api_key)
3997
- sonilo_base_url = text_setting("Sonilo Base URL", "sonilo_base_url")
3998
- st.markdown("**Suno agente musical opcional**")
3999
- suno_api_key = text_setting("Suno API key", "suno_api_key", secret=True)
4000
- _render_credential_status(suno_api_key)
4001
- 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.")
4002
- suno_api_endpoint = text_setting("Suno API endpoint", "suno_api_endpoint", help_text="Ex.: /api/generate")
4003
-
4004
- with st.expander("TikTok for Developers Client ID e Client Secret", expanded=False):
4005
- 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.")
4006
- tiktok_client_key = text_setting("TikTok Client ID", "tiktok_client_key", secret=True)
4007
- _render_credential_status(tiktok_client_key)
4008
- tiktok_client_secret = text_setting("TikTok Client Secret", "tiktok_client_secret", secret=True)
4009
-
4010
- with st.expander("Publicação através do Upload-Post", expanded=False):
4011
- upload_post_enabled = st.checkbox("Activar Upload-Post", bool(settings.get("upload_post_enabled", False)))
4012
- upload_post_api_key = text_setting("Upload-Post API key", "upload_post_api_key", secret=True)
4013
- _render_credential_status(upload_post_api_key)
4014
- upload_post_username = text_setting("Upload-Post username", "upload_post_username")
4015
- upload_post_platforms = text_setting("Plataformas Upload-Post", "upload_post_platforms")
4016
- upload_post_auto_upload = st.checkbox("Publicar automaticamente após gerar", bool(settings.get("upload_post_auto_upload", False)))
4017
-
4018
- with st.expander("Postiz API key, integração e MCP", expanded=False):
4019
- st.caption("O Thunderbolt é o cliente. A API key é enviada exclusivamente ao servidor Postiz configurado; não é colocada em URLs, logs ou repositório.")
4020
- postiz_enabled = st.checkbox("Activar Postiz como fallback final", bool(settings.get("postiz_enabled", False)))
4021
- postiz_mode = st.selectbox("Modo de ligação", ["api", "mcp"], index=0 if settings.get("postiz_mode", "api") != "mcp" else 1, help="API é o modo determinístico de upload. MCP fica disponível para uma ligação compatível com Streamable HTTP.")
4022
- postiz_cols = st.columns(2)
4023
- with postiz_cols[0]:
4024
- postiz_api_key = text_setting("Postiz API key", "postiz_api_key", secret=True, help_text="API key criada nas definições do Postiz. A API HTTP usa o valor bruto no cabeçalho Authorization.")
4025
- _render_credential_status(postiz_api_key)
4026
- postiz_base_url = text_setting("Postiz Public API Base URL", "postiz_base_url", help_text="Cloud: https://api.postiz.com/public/v1 · Self-hosted: https://seu-servidor/api/public/v1")
4027
- postiz_integration_id = text_setting("Postiz integração padrão", "postiz_integration_id", help_text="ID do canal/integração devolvido por GET /integrations.")
4028
- with postiz_cols[1]:
4029
- 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.")
4030
- postiz_auto_publish = st.checkbox("Permitir publicação imediata no Postiz", bool(settings.get("postiz_auto_publish", False)))
4031
- 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.")
4032
-
4033
- save_all_settings = st.form_submit_button("Guardar configurações do Thunderbolt", type="primary")
4034
- if save_all_settings:
4035
- settings.update({
4036
- "port": port, "moneyprinter_path": moneyprinter_path,
4037
- "kaggle_username": kaggle_username.strip(), "kaggle_api_key": kaggle_api_key.strip(), "kaggle_kernel_slug": kaggle_kernel_slug.strip() or "thunderbolt-niche-finder",
4038
- "apify_api_token": apify_api_token.strip(), "apify_actor_id": apify_actor_id.strip() or DEFAULT_ACTOR_ID, "apify_poll_interval_seconds": int(apify_poll_interval), "apify_run_timeout_seconds": int(apify_run_timeout),
4039
- "gemini_image_api_key": gemini_image_api_key, "gemini_image_model": gemini_image_model, "gemini_image_aspect_ratio": gemini_image_aspect_ratio, "gemini_image_size": gemini_image_size,
4040
- "azure_speech_key": azure_speech_key, "azure_speech_region": azure_speech_region,
4041
- "siliconflow_tts_api_key": siliconflow_tts_api_key, "minimax_tts_api_key": minimax_tts_api_key,
4042
- "minimax_tts_base_url": minimax_tts_base_url, "minimax_tts_model_id": minimax_tts_model_id, "minimax_tts_voice_id": minimax_tts_voice_id,
4043
- "elevenlabs_api_key": elevenlabs_api_key, "elevenlabs_model_id": elevenlabs_model_id,
4044
- "chatterbox_base_url": chatterbox_base_url, "chatterbox_api_key": chatterbox_api_key, "chatterbox_model_id": chatterbox_model_id,
4045
- "sonilo_api_key": sonilo_api_key, "sonilo_base_url": sonilo_base_url, "suno_api_key": suno_api_key, "suno_api_base_url": suno_api_base_url, "suno_api_endpoint": suno_api_endpoint,
4046
- "tiktok_client_key": tiktok_client_key, "tiktok_client_secret": tiktok_client_secret,
4047
- "upload_post_enabled": upload_post_enabled, "upload_post_api_key": upload_post_api_key,
4048
- "upload_post_username": upload_post_username, "upload_post_platforms": upload_post_platforms,
4049
- "upload_post_auto_upload": upload_post_auto_upload,
4050
- "postiz_enabled": postiz_enabled, "postiz_api_key": postiz_api_key, "postiz_base_url": postiz_base_url.strip() or "https://api.postiz.com/public/v1",
4051
- "postiz_mcp_url": postiz_mcp_url.strip() or "https://api.postiz.com/mcp", "postiz_mode": postiz_mode,
4052
- "postiz_integration_id": postiz_integration_id.strip(), "postiz_auto_publish": bool(postiz_auto_publish),
4053
- })
4054
- write_json("settings.json", settings)
4055
- try:
4056
- synced = sync_moneyprinter_config(settings, moneyprinter_path)
4057
- if synced:
4058
- st.success(f"Configurações guardadas e sincronizadas com {synced}")
4059
- else:
4060
- st.success("Configurações guardadas localmente. Indique uma pasta válida do motor de vídeo para sincronizar config.toml.")
4061
- except Exception as exc:
4062
- st.warning(f"Configurações locais guardadas, mas não foi possível sincronizar config.toml: {exc}")
3937
+ with st.container(border=True):
3938
+ st.subheader("API Keys")
3939
+ moneyprinter_path = str(settings.get("moneyprinter_path") or "").strip()
3940
+ st.caption(f"Pasta do motor de vídeo: `{moneyprinter_path or 'não configurada'}`")
3941
+ with st.form("settings_form"):
3942
+ with st.expander("Niche Finder Kaggle", expanded=False):
3943
+ st.caption("O dataset permanece no Kaggle. O Thunderbolt usa estas credenciais apenas para publicar/executar a kernel e obter os resultados pequenos da análise.")
3944
+ kaggle_cols = st.columns(3)
3945
+ with kaggle_cols[0]:
3946
+ kaggle_username = text_setting("Kaggle Username", "kaggle_username", help_text="Nome de utilizador da sua conta Kaggle, sem @ e sem URL.")
3947
+ with kaggle_cols[1]:
3948
+ kaggle_api_key = text_setting("Kaggle API Key", "kaggle_api_key", secret=True, help_text="Chave criada em Kaggle > Settings > API. Nunca é incluída no notebook ou no GitHub.")
3949
+ with kaggle_cols[2]:
3950
+ kaggle_kernel_slug = text_setting("Slug da kernel", "kaggle_kernel_slug", help_text="Identificador da kernel remota, por exemplo thunderbolt-niche-finder.")
3951
+ _render_credential_status(kaggle_api_key)
3952
+
3953
+ with st.expander("Niche Finder Apify", expanded=False):
3954
+ 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.")
3955
+ apify_cols = st.columns(4)
3956
+ with apify_cols[0]:
3957
+ apify_api_token = text_setting("Apify API Token", "apify_api_token", secret=True, help_text="Token pessoal da Apify. Não é incluído no workflow, logs ou GitHub.")
3958
+ with apify_cols[1]:
3959
+ apify_actor_id = text_setting("Apify Actor ID", "apify_actor_id", help_text="Por padrão: streamers~youtube-scraper.")
3960
+ with apify_cols[2]:
3961
+ apify_poll_interval = st.number_input("Intervalo de consulta (s)", min_value=1, max_value=120, value=int(settings.get("apify_poll_interval_seconds", 10)), step=1)
3962
+ with apify_cols[3]:
3963
+ 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)
3964
+ _render_credential_status(apify_api_token)
3965
+
3966
+ render_llm_provider_cards(settings, embedded=True)
3967
+
3968
+ with st.expander("Nano Banana — geração de thumbnails", expanded=False):
3969
+ st.caption("A Nano Banana gera a imagem final das thumbnails a partir da variante escolhida. A chave é guardada apenas no storage local e é distinta da chave do Gemini usado como LLM textual.")
3970
+ nano_cols = st.columns(2)
3971
+ with nano_cols[0]:
3972
+ gemini_image_api_key = text_setting("Nano Banana API key", "gemini_image_api_key", secret=True, help_text="Chave criada no Google AI Studio para a API Gemini. Nunca é incluída no código, logs ou pacote.")
3973
+ gemini_image_model = st.selectbox("Modelo Nano Banana", ["gemini-3.1-flash-image", "gemini-3-pro-image", "gemini-2.5-flash-image"], index=["gemini-3.1-flash-image", "gemini-3-pro-image", "gemini-2.5-flash-image"].index(str(settings.get("gemini_image_model") or "gemini-3.1-flash-image")) if str(settings.get("gemini_image_model") or "gemini-3.1-flash-image") in {"gemini-3.1-flash-image", "gemini-3-pro-image", "gemini-2.5-flash-image"} else 0)
3974
+ with nano_cols[1]:
3975
+ 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)
3976
+ 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)
3977
+ _render_credential_status(gemini_image_api_key)
3978
+
3979
+
3980
+ with st.expander("Voz, TTS e música — Azure Speech, restantes serviços e Suno", expanded=False):
3981
+ cols = st.columns(2)
3982
+ with cols[0]:
3983
+ azure_speech_key = text_setting("Azure Speech key", "azure_speech_key", secret=True)
3984
+ _render_credential_status(azure_speech_key)
3985
+ azure_speech_region = text_setting("Azure Speech region", "azure_speech_region")
3986
+ siliconflow_tts_api_key = text_setting("SiliconFlow TTS API key", "siliconflow_tts_api_key", secret=True)
3987
+ _render_credential_status(siliconflow_tts_api_key)
3988
+ minimax_tts_api_key = text_setting("MiniMax TTS API key", "minimax_tts_api_key", secret=True)
3989
+ _render_credential_status(minimax_tts_api_key)
3990
+ minimax_tts_base_url = text_setting("MiniMax TTS Base URL", "minimax_tts_base_url")
3991
+ minimax_tts_model_id = text_setting("MiniMax TTS model", "minimax_tts_model_id")
3992
+ minimax_tts_voice_id = text_setting("MiniMax TTS voice ID", "minimax_tts_voice_id")
3993
+ with cols[1]:
3994
+ elevenlabs_api_key = text_setting("ElevenLabs API key", "elevenlabs_api_key", secret=True)
3995
+ _render_credential_status(elevenlabs_api_key)
3996
+ elevenlabs_model_id = text_setting("ElevenLabs model", "elevenlabs_model_id")
3997
+ chatterbox_base_url = text_setting("Chatterbox Base URL", "chatterbox_base_url")
3998
+ chatterbox_api_key = text_setting("Chatterbox API key", "chatterbox_api_key", secret=True)
3999
+ _render_credential_status(chatterbox_api_key, local=True, required=False)
4000
+ chatterbox_model_id = text_setting("Chatterbox model", "chatterbox_model_id")
4001
+ sonilo_api_key = text_setting("Sonilo API key", "sonilo_api_key", secret=True)
4002
+ _render_credential_status(sonilo_api_key)
4003
+ sonilo_base_url = text_setting("Sonilo Base URL", "sonilo_base_url")
4004
+ st.markdown("**Suno agente musical opcional**")
4005
+ suno_api_key = text_setting("Suno API key", "suno_api_key", secret=True)
4006
+ _render_credential_status(suno_api_key)
4007
+ 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.")
4008
+ suno_api_endpoint = text_setting("Suno API endpoint", "suno_api_endpoint", help_text="Ex.: /api/generate")
4009
+
4010
+ with st.expander("TikTok for Developers — Client ID e Client Secret", expanded=False):
4011
+ 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.")
4012
+ tiktok_client_key = text_setting("TikTok Client ID", "tiktok_client_key", secret=True)
4013
+ _render_credential_status(tiktok_client_key)
4014
+ tiktok_client_secret = text_setting("TikTok Client Secret", "tiktok_client_secret", secret=True)
4015
+
4016
+ with st.expander("Publicação através do Upload-Post", expanded=False):
4017
+ upload_post_enabled = st.checkbox("Activar Upload-Post", bool(settings.get("upload_post_enabled", False)))
4018
+ upload_post_api_key = text_setting("Upload-Post API key", "upload_post_api_key", secret=True)
4019
+ _render_credential_status(upload_post_api_key)
4020
+ upload_post_username = text_setting("Upload-Post username", "upload_post_username")
4021
+ upload_post_platforms = text_setting("Plataformas Upload-Post", "upload_post_platforms")
4022
+ upload_post_auto_upload = st.checkbox("Publicar automaticamente após gerar", bool(settings.get("upload_post_auto_upload", False)))
4023
+
4024
+ with st.expander("Postiz — API key, integração e MCP", expanded=False):
4025
+ st.caption("O Thunderbolt é o cliente. A API key é enviada exclusivamente ao servidor Postiz configurado; não é colocada em URLs, logs ou repositório.")
4026
+ postiz_enabled = st.checkbox("Activar Postiz como fallback final", bool(settings.get("postiz_enabled", False)))
4027
+ postiz_mode = st.selectbox("Modo de ligação", ["api", "mcp"], index=0 if settings.get("postiz_mode", "api") != "mcp" else 1, help="API é o modo determinístico de upload. MCP fica disponível para uma ligação compatível com Streamable HTTP.")
4028
+ postiz_cols = st.columns(2)
4029
+ with postiz_cols[0]:
4030
+ postiz_api_key = text_setting("Postiz API key", "postiz_api_key", secret=True, help_text="API key criada nas definições do Postiz. A API HTTP usa o valor bruto no cabeçalho Authorization.")
4031
+ _render_credential_status(postiz_api_key)
4032
+ postiz_base_url = text_setting("Postiz Public API Base URL", "postiz_base_url", help_text="Cloud: https://api.postiz.com/public/v1 · Self-hosted: https://seu-servidor/api/public/v1")
4033
+ postiz_integration_id = text_setting("Postiz integração padrão", "postiz_integration_id", help_text="ID do canal/integração devolvido por GET /integrations.")
4034
+ with postiz_cols[1]:
4035
+ 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.")
4036
+ postiz_auto_publish = st.checkbox("Permitir publicação imediata no Postiz", bool(settings.get("postiz_auto_publish", False)))
4037
+ 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.")
4038
+
4039
+ save_all_settings = st.form_submit_button("Guardar configurações do Thunderbolt", type="primary")
4040
+ if save_all_settings:
4041
+ settings.update({
4042
+ "moneyprinter_path": moneyprinter_path,
4043
+ "kaggle_username": kaggle_username.strip(), "kaggle_api_key": kaggle_api_key.strip(), "kaggle_kernel_slug": kaggle_kernel_slug.strip() or "thunderbolt-niche-finder",
4044
+ "apify_api_token": apify_api_token.strip(), "apify_actor_id": apify_actor_id.strip() or DEFAULT_ACTOR_ID, "apify_poll_interval_seconds": int(apify_poll_interval), "apify_run_timeout_seconds": int(apify_run_timeout),
4045
+ "gemini_image_api_key": gemini_image_api_key, "gemini_image_model": gemini_image_model, "gemini_image_aspect_ratio": gemini_image_aspect_ratio, "gemini_image_size": gemini_image_size,
4046
+ "azure_speech_key": azure_speech_key, "azure_speech_region": azure_speech_region,
4047
+ "siliconflow_tts_api_key": siliconflow_tts_api_key, "minimax_tts_api_key": minimax_tts_api_key,
4048
+ "minimax_tts_base_url": minimax_tts_base_url, "minimax_tts_model_id": minimax_tts_model_id, "minimax_tts_voice_id": minimax_tts_voice_id,
4049
+ "elevenlabs_api_key": elevenlabs_api_key, "elevenlabs_model_id": elevenlabs_model_id,
4050
+ "chatterbox_base_url": chatterbox_base_url, "chatterbox_api_key": chatterbox_api_key, "chatterbox_model_id": chatterbox_model_id,
4051
+ "sonilo_api_key": sonilo_api_key, "sonilo_base_url": sonilo_base_url, "suno_api_key": suno_api_key, "suno_api_base_url": suno_api_base_url, "suno_api_endpoint": suno_api_endpoint,
4052
+ "tiktok_client_key": tiktok_client_key, "tiktok_client_secret": tiktok_client_secret,
4053
+ "upload_post_enabled": upload_post_enabled, "upload_post_api_key": upload_post_api_key,
4054
+ "upload_post_username": upload_post_username, "upload_post_platforms": upload_post_platforms,
4055
+ "upload_post_auto_upload": upload_post_auto_upload,
4056
+ "postiz_enabled": postiz_enabled, "postiz_api_key": postiz_api_key, "postiz_base_url": postiz_base_url.strip() or "https://api.postiz.com/public/v1",
4057
+ "postiz_mcp_url": postiz_mcp_url.strip() or "https://api.postiz.com/mcp", "postiz_mode": postiz_mode,
4058
+ "postiz_integration_id": postiz_integration_id.strip(), "postiz_auto_publish": bool(postiz_auto_publish),
4059
+ })
4060
+ write_json("settings.json", settings)
4061
+ try:
4062
+ synced = sync_moneyprinter_config(settings, moneyprinter_path)
4063
+ if synced:
4064
+ st.success(f"Configurações guardadas e sincronizadas com {synced}")
4065
+ else:
4066
+ st.success("Configurações guardadas localmente. Indique uma pasta válida do motor de vídeo para sincronizar config.toml.")
4067
+ except Exception as exc:
4068
+ st.warning(f"Configurações locais guardadas, mas não foi possível sincronizar config.toml: {exc}")
4063
4069
  with material_sources_tab:
4064
4070
  render_material_source_api_keys(settings)
4065
4071
 
4066
4072
  with voice_test_tab:
4067
- st.subheader("Teste de vozes")
4073
+ st.subheader("Teste de Voz")
4068
4074
  st.caption("Este painel é exclusivamente um preview. O áudio gerado não altera vídeos, tarefas, Blueprints ou a configuração da pipeline.")
4069
4075
  preview_cols = st.columns([1.1, 2.2, 1.2])
4070
4076
  with preview_cols[0]:
@@ -412,37 +412,37 @@ _TAB_LABELS = (
412
412
  "Importar do YouTube", "Canais em lote gmail", "Criar vídeo", "Vídeos", "Novo roteiro/letra", "Histórico guardado",
413
413
  "Clusters encontrados", "Regras de associação", "Dados analisados", "Upload ficheiro", "URL de vídeo", "Vídeos gerados",
414
414
  "Pasta local", "Código Python", "Upload convencional", "Upload directo", "Postiz", "Upload-Post", "API Keys",
415
- "Teste de vozes", "Serviços e modelos", "Fontes de materiais", "Client MCP", "Servidor MCP", "Skill",
415
+ "Teste de Voz", "Serviços e modelos", "Fontes de Materiais", "Client MCP", "Servidor MCP", "Skill",
416
416
  )
417
417
 
418
418
  TAB_TRANSLATIONS: dict[str, dict[str, str]] = {
419
419
  "pt": {label: label for label in _TAB_LABELS},
420
420
  "en": {
421
- "Blueprints": "Blueprints", "Brandings": "Brandings", "Pesquisa pública": "Public search", "Cadastro manual": "Manual registration", "Contas cadastradas": "Registered accounts", "Biblioteca": "Library", "Importar do YouTube": "Import from YouTube", "Canais em lote gmail": "Bulk Gmail channels", "Criar vídeo": "Create video", "Vídeos": "Videos", "Novo roteiro/letra": "New script/lyrics", "Histórico guardado": "Saved history", "Clusters encontrados": "Found clusters", "Regras de associação": "Association rules", "Dados analisados": "Analyzed data", "Upload ficheiro": "Upload file", "URL de vídeo": "Video URL", "Vídeos gerados": "Generated videos", "Pasta local": "Local folder", "Código Python": "Python code", "Upload convencional": "Conventional upload", "Upload directo": "Direct upload", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "API Keys", "Teste de vozes": "Voice testing", "Serviços e modelos": "Services and models", "Fontes de materiais": "Media sources", "Client MCP": "MCP client", "Servidor MCP": "MCP server", "Skill": "Skill",
421
+ "Blueprints": "Blueprints", "Brandings": "Brandings", "Pesquisa pública": "Public search", "Cadastro manual": "Manual registration", "Contas cadastradas": "Registered accounts", "Biblioteca": "Library", "Importar do YouTube": "Import from YouTube", "Canais em lote gmail": "Bulk Gmail channels", "Criar vídeo": "Create video", "Vídeos": "Videos", "Novo roteiro/letra": "New script/lyrics", "Histórico guardado": "Saved history", "Clusters encontrados": "Found clusters", "Regras de associação": "Association rules", "Dados analisados": "Analyzed data", "Upload ficheiro": "Upload file", "URL de vídeo": "Video URL", "Vídeos gerados": "Generated videos", "Pasta local": "Local folder", "Código Python": "Python code", "Upload convencional": "Conventional upload", "Upload directo": "Direct upload", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "API Keys", "Teste de Voz": "Voice testing", "Serviços e modelos": "Services and models", "Fontes de Materiais": "Media sources", "Client MCP": "MCP client", "Servidor MCP": "MCP server", "Skill": "Skill",
422
422
  },
423
423
  "zh": {
424
- "Blueprints": "蓝图", "Brandings": "品牌", "Pesquisa pública": "公开搜索", "Cadastro manual": "手动注册", "Contas cadastradas": "已注册账户", "Biblioteca": "库", "Importar do YouTube": "从 YouTube 导入", "Canais em lote gmail": "Gmail 批量频道", "Criar vídeo": "创建视频", "Vídeos": "视频", "Novo roteiro/letra": "新建脚本/歌词", "Histórico guardado": "已保存历史", "Clusters encontrados": "找到的聚类", "Regras de associação": "关联规则", "Dados analisados": "分析数据", "Upload ficheiro": "上传文件", "URL de vídeo": "视频 URL", "Vídeos gerados": "已生成视频", "Pasta local": "本地文件夹", "Código Python": "Python 代码", "Upload convencional": "常规上传", "Upload directo": "直接上传", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "API 密钥", "Teste de vozes": "语音测试", "Serviços e modelos": "服务与模型", "Fontes de materiais": "媒体来源", "Client MCP": "MCP 客户端", "Servidor MCP": "MCP 服务器", "Skill": "技能",
424
+ "Blueprints": "蓝图", "Brandings": "品牌", "Pesquisa pública": "公开搜索", "Cadastro manual": "手动注册", "Contas cadastradas": "已注册账户", "Biblioteca": "库", "Importar do YouTube": "从 YouTube 导入", "Canais em lote gmail": "Gmail 批量频道", "Criar vídeo": "创建视频", "Vídeos": "视频", "Novo roteiro/letra": "新建脚本/歌词", "Histórico guardado": "已保存历史", "Clusters encontrados": "找到的聚类", "Regras de associação": "关联规则", "Dados analisados": "分析数据", "Upload ficheiro": "上传文件", "URL de vídeo": "视频 URL", "Vídeos gerados": "已生成视频", "Pasta local": "本地文件夹", "Código Python": "Python 代码", "Upload convencional": "常规上传", "Upload directo": "直接上传", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "API 密钥", "Teste de Voz": "语音测试", "Serviços e modelos": "服务与模型", "Fontes de Materiais": "媒体来源", "Client MCP": "MCP 客户端", "Servidor MCP": "MCP 服务器", "Skill": "技能",
425
425
  },
426
426
  "de": {
427
- "Blueprints": "Blueprints", "Brandings": "Brandings", "Pesquisa pública": "Öffentliche Suche", "Cadastro manual": "Manuelle Registrierung", "Contas cadastradas": "Registrierte Konten", "Biblioteca": "Bibliothek", "Importar do YouTube": "Von YouTube importieren", "Canais em lote gmail": "Gmail-Kanäle im Stapel", "Criar vídeo": "Video erstellen", "Vídeos": "Videos", "Novo roteiro/letra": "Neues Skript/Liedtext", "Histórico guardado": "Gespeicherter Verlauf", "Clusters encontrados": "Gefundene Cluster", "Regras de associação": "Assoziationsregeln", "Dados analisados": "Analysierte Daten", "Upload ficheiro": "Datei hochladen", "URL de vídeo": "Video-URL", "Vídeos gerados": "Erstellte Videos", "Pasta local": "Lokaler Ordner", "Código Python": "Python-Code", "Upload convencional": "Herkömmlicher Upload", "Upload directo": "Direkter Upload", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "API-Schlüssel", "Teste de vozes": "Stimmtest", "Serviços e modelos": "Dienste und Modelle", "Fontes de materiais": "Medienquellen", "Client MCP": "MCP-Client", "Servidor MCP": "MCP-Server", "Skill": "Skill",
427
+ "Blueprints": "Blueprints", "Brandings": "Brandings", "Pesquisa pública": "Öffentliche Suche", "Cadastro manual": "Manuelle Registrierung", "Contas cadastradas": "Registrierte Konten", "Biblioteca": "Bibliothek", "Importar do YouTube": "Von YouTube importieren", "Canais em lote gmail": "Gmail-Kanäle im Stapel", "Criar vídeo": "Video erstellen", "Vídeos": "Videos", "Novo roteiro/letra": "Neues Skript/Liedtext", "Histórico guardado": "Gespeicherter Verlauf", "Clusters encontrados": "Gefundene Cluster", "Regras de associação": "Assoziationsregeln", "Dados analisados": "Analysierte Daten", "Upload ficheiro": "Datei hochladen", "URL de vídeo": "Video-URL", "Vídeos gerados": "Erstellte Videos", "Pasta local": "Lokaler Ordner", "Código Python": "Python-Code", "Upload convencional": "Herkömmlicher Upload", "Upload directo": "Direkter Upload", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "API-Schlüssel", "Teste de Voz": "Stimmtest", "Serviços e modelos": "Dienste und Modelle", "Fontes de Materiais": "Medienquellen", "Client MCP": "MCP-Client", "Servidor MCP": "MCP-Server", "Skill": "Skill",
428
428
  },
429
429
  "vi": {
430
- "Blueprints": "Blueprint", "Brandings": "Thương hiệu", "Pesquisa pública": "Tìm kiếm công khai", "Cadastro manual": "Đăng ký thủ công", "Contas cadastradas": "Tài khoản đã đăng ký", "Biblioteca": "Thư viện", "Importar do YouTube": "Nhập từ YouTube", "Canais em lote gmail": "Kênh Gmail hàng loạt", "Criar vídeo": "Tạo video", "Vídeos": "Video", "Novo roteiro/letra": "Kịch bản/lời bài hát mới", "Histórico guardado": "Lịch sử đã lưu", "Clusters encontrados": "Cụm được tìm thấy", "Regras de associação": "Quy tắc liên kết", "Dados analisados": "Dữ liệu đã phân tích", "Upload ficheiro": "Tải tệp lên", "URL de vídeo": "URL video", "Vídeos gerados": "Video đã tạo", "Pasta local": "Thư mục cục bộ", "Código Python": "Mã Python", "Upload convencional": "Tải lên thông thường", "Upload directo": "Tải lên trực tiếp", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "Khóa API", "Teste de vozes": "Kiểm tra giọng nói", "Serviços e modelos": "Dịch vụ và mô hình", "Fontes de materiais": "Nguồn phương tiện", "Client MCP": "Máy khách MCP", "Servidor MCP": "Máy chủ MCP", "Skill": "Kỹ năng",
430
+ "Blueprints": "Blueprint", "Brandings": "Thương hiệu", "Pesquisa pública": "Tìm kiếm công khai", "Cadastro manual": "Đăng ký thủ công", "Contas cadastradas": "Tài khoản đã đăng ký", "Biblioteca": "Thư viện", "Importar do YouTube": "Nhập từ YouTube", "Canais em lote gmail": "Kênh Gmail hàng loạt", "Criar vídeo": "Tạo video", "Vídeos": "Video", "Novo roteiro/letra": "Kịch bản/lời bài hát mới", "Histórico guardado": "Lịch sử đã lưu", "Clusters encontrados": "Cụm được tìm thấy", "Regras de associação": "Quy tắc liên kết", "Dados analisados": "Dữ liệu đã phân tích", "Upload ficheiro": "Tải tệp lên", "URL de vídeo": "URL video", "Vídeos gerados": "Video đã tạo", "Pasta local": "Thư mục cục bộ", "Código Python": "Mã Python", "Upload convencional": "Tải lên thông thường", "Upload directo": "Tải lên trực tiếp", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "Khóa API", "Teste de Voz": "Kiểm tra giọng nói", "Serviços e modelos": "Dịch vụ và mô hình", "Fontes de Materiais": "Nguồn phương tiện", "Client MCP": "Máy khách MCP", "Servidor MCP": "Máy chủ MCP", "Skill": "Kỹ năng",
431
431
  },
432
432
  "tr": {
433
- "Blueprints": "Blueprint'ler", "Brandings": "Markalar", "Pesquisa pública": "Herkese açık arama", "Cadastro manual": "Manuel kayıt", "Contas cadastradas": "Kayıtlı hesaplar", "Biblioteca": "Kütüphane", "Importar do YouTube": "YouTube'dan içe aktar", "Canais em lote gmail": "Toplu Gmail kanalları", "Criar vídeo": "Video oluştur", "Vídeos": "Videolar", "Novo roteiro/letra": "Yeni senaryo/şarkı sözü", "Histórico guardado": "Kayıtlı geçmiş", "Clusters encontrados": "Bulunan kümeler", "Regras de associação": "Birliktelik kuralları", "Dados analisados": "Analiz edilen veriler", "Upload ficheiro": "Dosya yükle", "URL de vídeo": "Video URL'si", "Vídeos gerados": "Oluşturulan videolar", "Pasta local": "Yerel klasör", "Código Python": "Python kodu", "Upload convencional": "Geleneksel yükleme", "Upload directo": "Doğrudan yükleme", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "API anahtarları", "Teste de vozes": "Ses testi", "Serviços e modelos": "Hizmetler ve modeller", "Fontes de materiais": "Medya kaynakları", "Client MCP": "MCP istemcisi", "Servidor MCP": "MCP sunucusu", "Skill": "Beceri",
433
+ "Blueprints": "Blueprint'ler", "Brandings": "Markalar", "Pesquisa pública": "Herkese açık arama", "Cadastro manual": "Manuel kayıt", "Contas cadastradas": "Kayıtlı hesaplar", "Biblioteca": "Kütüphane", "Importar do YouTube": "YouTube'dan içe aktar", "Canais em lote gmail": "Toplu Gmail kanalları", "Criar vídeo": "Video oluştur", "Vídeos": "Videolar", "Novo roteiro/letra": "Yeni senaryo/şarkı sözü", "Histórico guardado": "Kayıtlı geçmiş", "Clusters encontrados": "Bulunan kümeler", "Regras de associação": "Birliktelik kuralları", "Dados analisados": "Analiz edilen veriler", "Upload ficheiro": "Dosya yükle", "URL de vídeo": "Video URL'si", "Vídeos gerados": "Oluşturulan videolar", "Pasta local": "Yerel klasör", "Código Python": "Python kodu", "Upload convencional": "Geleneksel yükleme", "Upload directo": "Doğrudan yükleme", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "API anahtarları", "Teste de Voz": "Ses testi", "Serviços e modelos": "Hizmetler ve modeller", "Fontes de Materiais": "Medya kaynakları", "Client MCP": "MCP istemcisi", "Servidor MCP": "MCP sunucusu", "Skill": "Beceri",
434
434
  },
435
435
  "ru": {
436
- "Blueprints": "Blueprints", "Brandings": "Брендинг", "Pesquisa pública": "Публичный поиск", "Cadastro manual": "Ручная регистрация", "Contas cadastradas": "Зарегистрированные аккаунты", "Biblioteca": "Библиотека", "Importar do YouTube": "Импорт из YouTube", "Canais em lote gmail": "Массовые каналы Gmail", "Criar vídeo": "Создать видео", "Vídeos": "Видео", "Novo roteiro/letra": "Новый сценарий/текст", "Histórico guardado": "Сохранённая история", "Clusters encontrados": "Найденные кластеры", "Regras de associação": "Правила ассоциаций", "Dados analisados": "Анализ данных", "Upload ficheiro": "Загрузить файл", "URL de vídeo": "URL видео", "Vídeos gerados": "Созданные видео", "Pasta local": "Локальная папка", "Código Python": "Код Python", "Upload convencional": "Обычная загрузка", "Upload directo": "Прямая загрузка", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "Ключи API", "Teste de vozes": "Тест голосов", "Serviços e modelos": "Сервисы и модели", "Fontes de materiais": "Источники материалов", "Client MCP": "Клиент MCP", "Servidor MCP": "Сервер MCP", "Skill": "Навык",
436
+ "Blueprints": "Blueprints", "Brandings": "Брендинг", "Pesquisa pública": "Публичный поиск", "Cadastro manual": "Ручная регистрация", "Contas cadastradas": "Зарегистрированные аккаунты", "Biblioteca": "Библиотека", "Importar do YouTube": "Импорт из YouTube", "Canais em lote gmail": "Массовые каналы Gmail", "Criar vídeo": "Создать видео", "Vídeos": "Видео", "Novo roteiro/letra": "Новый сценарий/текст", "Histórico guardado": "Сохранённая история", "Clusters encontrados": "Найденные кластеры", "Regras de associação": "Правила ассоциаций", "Dados analisados": "Анализ данных", "Upload ficheiro": "Загрузить файл", "URL de vídeo": "URL видео", "Vídeos gerados": "Созданные видео", "Pasta local": "Локальная папка", "Código Python": "Код Python", "Upload convencional": "Обычная загрузка", "Upload directo": "Прямая загрузка", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "Ключи API", "Teste de Voz": "Тест голосов", "Serviços e modelos": "Сервисы и модели", "Fontes de Materiais": "Источники материалов", "Client MCP": "Клиент MCP", "Servidor MCP": "Сервер MCP", "Skill": "Навык",
437
437
  },
438
438
  "es": {
439
- "Blueprints": "Blueprints", "Brandings": "Marcas", "Pesquisa pública": "Búsqueda pública", "Cadastro manual": "Registro manual", "Contas cadastradas": "Cuentas registradas", "Biblioteca": "Biblioteca", "Importar do YouTube": "Importar de YouTube", "Canais em lote gmail": "Canales Gmail por lotes", "Criar vídeo": "Crear vídeo", "Vídeos": "Vídeos", "Novo roteiro/letra": "Nuevo guion/letra", "Histórico guardado": "Historial guardado", "Clusters encontrados": "Clústeres encontrados", "Regras de associação": "Reglas de asociación", "Dados analisados": "Datos analizados", "Upload ficheiro": "Subir archivo", "URL de vídeo": "URL del vídeo", "Vídeos gerados": "Vídeos generados", "Pasta local": "Carpeta local", "Código Python": "Código Python", "Upload convencional": "Carga convencional", "Upload directo": "Carga directa", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "Claves API", "Teste de vozes": "Prueba de voces", "Serviços e modelos": "Servicios y modelos", "Fontes de materiais": "Fuentes de medios", "Client MCP": "Cliente MCP", "Servidor MCP": "Servidor MCP", "Skill": "Habilidad",
439
+ "Blueprints": "Blueprints", "Brandings": "Marcas", "Pesquisa pública": "Búsqueda pública", "Cadastro manual": "Registro manual", "Contas cadastradas": "Cuentas registradas", "Biblioteca": "Biblioteca", "Importar do YouTube": "Importar de YouTube", "Canais em lote gmail": "Canales Gmail por lotes", "Criar vídeo": "Crear vídeo", "Vídeos": "Vídeos", "Novo roteiro/letra": "Nuevo guion/letra", "Histórico guardado": "Historial guardado", "Clusters encontrados": "Clústeres encontrados", "Regras de associação": "Reglas de asociación", "Dados analisados": "Datos analizados", "Upload ficheiro": "Subir archivo", "URL de vídeo": "URL del vídeo", "Vídeos gerados": "Vídeos generados", "Pasta local": "Carpeta local", "Código Python": "Código Python", "Upload convencional": "Carga convencional", "Upload directo": "Carga directa", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "Claves API", "Teste de Voz": "Prueba de voces", "Serviços e modelos": "Servicios y modelos", "Fontes de Materiais": "Fuentes de medios", "Client MCP": "Cliente MCP", "Servidor MCP": "Servidor MCP", "Skill": "Habilidad",
440
440
  },
441
441
  "id": {
442
- "Blueprints": "Blueprint", "Brandings": "Branding", "Pesquisa pública": "Pencarian publik", "Cadastro manual": "Pendaftaran manual", "Contas cadastradas": "Akun terdaftar", "Biblioteca": "Pustaka", "Importar do YouTube": "Impor dari YouTube", "Canais em lote gmail": "Kanal Gmail massal", "Criar vídeo": "Buat video", "Vídeos": "Video", "Novo roteiro/letra": "Skrip/lirik baru", "Histórico guardado": "Riwayat tersimpan", "Clusters encontrados": "Cluster ditemukan", "Regras de associação": "Aturan asosiasi", "Dados analisados": "Data yang dianalisis", "Upload ficheiro": "Unggah file", "URL de vídeo": "URL video", "Vídeos gerados": "Video yang dibuat", "Pasta local": "Folder lokal", "Código Python": "Kode Python", "Upload convencional": "Unggah konvensional", "Upload directo": "Unggah langsung", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "Kunci API", "Teste de vozes": "Uji suara", "Serviços e modelos": "Layanan dan model", "Fontes de materiais": "Sumber media", "Client MCP": "Klien MCP", "Servidor MCP": "Server MCP", "Skill": "Keahlian",
442
+ "Blueprints": "Blueprint", "Brandings": "Branding", "Pesquisa pública": "Pencarian publik", "Cadastro manual": "Pendaftaran manual", "Contas cadastradas": "Akun terdaftar", "Biblioteca": "Pustaka", "Importar do YouTube": "Impor dari YouTube", "Canais em lote gmail": "Kanal Gmail massal", "Criar vídeo": "Buat video", "Vídeos": "Video", "Novo roteiro/letra": "Skrip/lirik baru", "Histórico guardado": "Riwayat tersimpan", "Clusters encontrados": "Cluster ditemukan", "Regras de associação": "Aturan asosiasi", "Dados analisados": "Data yang dianalisis", "Upload ficheiro": "Unggah file", "URL de vídeo": "URL video", "Vídeos gerados": "Video yang dibuat", "Pasta local": "Folder lokal", "Código Python": "Kode Python", "Upload convencional": "Unggah konvensional", "Upload directo": "Unggah langsung", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "Kunci API", "Teste de Voz": "Uji suara", "Serviços e modelos": "Layanan dan model", "Fontes de Materiais": "Sumber media", "Client MCP": "Klien MCP", "Servidor MCP": "Server MCP", "Skill": "Keahlian",
443
443
  },
444
444
  "it": {
445
- "Blueprints": "Blueprint", "Brandings": "Branding", "Pesquisa pública": "Ricerca pubblica", "Cadastro manual": "Registrazione manuale", "Contas cadastradas": "Account registrati", "Biblioteca": "Libreria", "Importar do YouTube": "Importa da YouTube", "Canais em lote gmail": "Canali Gmail in batch", "Criar vídeo": "Crea video", "Vídeos": "Video", "Novo roteiro/letra": "Nuovo copione/testo", "Histórico guardado": "Cronologia salvata", "Clusters encontrados": "Cluster trovati", "Regras de associação": "Regole di associazione", "Dados analisados": "Dati analizzati", "Upload ficheiro": "Carica file", "URL de vídeo": "URL video", "Vídeos gerados": "Video generati", "Pasta local": "Cartella locale", "Código Python": "Codice Python", "Upload convencional": "Caricamento convenzionale", "Upload directo": "Caricamento diretto", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "Chiavi API", "Teste de vozes": "Test delle voci", "Serviços e modelos": "Servizi e modelli", "Fontes de materiais": "Fonti multimediali", "Client MCP": "Client MCP", "Servidor MCP": "Server MCP", "Skill": "Skill",
445
+ "Blueprints": "Blueprint", "Brandings": "Branding", "Pesquisa pública": "Ricerca pubblica", "Cadastro manual": "Registrazione manuale", "Contas cadastradas": "Account registrati", "Biblioteca": "Libreria", "Importar do YouTube": "Importa da YouTube", "Canais em lote gmail": "Canali Gmail in batch", "Criar vídeo": "Crea video", "Vídeos": "Video", "Novo roteiro/letra": "Nuovo copione/testo", "Histórico guardado": "Cronologia salvata", "Clusters encontrados": "Cluster trovati", "Regras de associação": "Regole di associazione", "Dados analisados": "Dati analizzati", "Upload ficheiro": "Carica file", "URL de vídeo": "URL video", "Vídeos gerados": "Video generati", "Pasta local": "Cartella locale", "Código Python": "Codice Python", "Upload convencional": "Caricamento convenzionale", "Upload directo": "Caricamento diretto", "Postiz": "Postiz", "Upload-Post": "Upload-Post", "API Keys": "Chiavi API", "Teste de Voz": "Test delle voci", "Serviços e modelos": "Servizi e modelli", "Fontes de Materiais": "Fonti multimediali", "Client MCP": "Client MCP", "Servidor MCP": "Server MCP", "Skill": "Skill",
446
446
  },
447
447
  }
448
448
 
@@ -565,9 +565,9 @@ _CONTENT_TRANSLATION_ROWS = (
565
565
  ("E-mail/Gmail da conta", "E-mail/Gmail da conta", "Account email/Gmail", "账户电子邮件/Gmail", "Konto-E-Mail/Gmail", "Email/Gmail tài khoản", "Hesap e-postası/Gmail", "Электронная почта/Gmail аккаунта", "Correo/Gmail de la cuenta", "Email/Gmail akun", "Email/Gmail dell'account"),
566
566
  ("Guardar dados da conta Google", "Guardar dados da conta Google", "Save Google account data", "保存 Google 账户数据", "Google-Kontodaten speichern", "Lưu dữ liệu tài khoản Google", "Google hesap verilerini kaydet", "Сохранить данные аккаунта Google", "Guardar datos de la cuenta de Google", "Simpan data akun Google", "Salva dati account Google"),
567
567
  ("API Keys", "API Keys", "API Keys", "API 密钥", "API-Schlüssel", "Khóa API", "API Anahtarları", "Ключи API", "Claves API", "Kunci API", "Chiavi API"),
568
- ("Teste de vozes", "Teste de vozes", "Voice testing", "语音测试", "Stimmtest", "Kiểm tra giọng nói", "Ses testi", "Тест голосов", "Prueba de voces", "Uji suara", "Test delle voci"),
568
+ ("Teste de Voz", "Teste de Voz", "Voice testing", "语音测试", "Stimmtest", "Kiểm tra giọng nói", "Ses testi", "Тест голосов", "Prueba de voces", "Uji suara", "Test delle voci"),
569
569
  ("Serviços e modelos", "Serviços e modelos", "Services and models", "服务与模型", "Dienste und Modelle", "Dịch vụ và mô hình", "Hizmetler ve modeller", "Сервисы и модели", "Servicios y modelos", "Layanan dan model", "Servizi e modelli"),
570
- ("Fontes de materiais", "Fontes de materiais", "Media sources", "媒体来源", "Medienquellen", "Nguồn phương tiện", "Medya kaynakları", "Источники материалов", "Fuentes de medios", "Sumber media", "Fonti multimediali"),
570
+ ("Fontes de Materiais", "Fontes de Materiais", "Media sources", "媒体来源", "Medienquellen", "Nguồn phương tiện", "Medya kaynakları", "Источники материалов", "Fuentes de medios", "Sumber media", "Fonti multimediali"),
571
571
  ("Notificações", "Notificações", "Notifications", "通知", "Benachrichtigungen", "Thông báo", "Bildirimler", "Уведомления", "Notificaciones", "Notifikasi", "Notifiche"),
572
572
  ("Client MCP", "Client MCP", "MCP client", "MCP 客户端", "MCP-Client", "Máy khách MCP", "MCP istemcisi", "Клиент MCP", "Cliente MCP", "Klien MCP", "Client MCP"),
573
573
  ("Servidor MCP", "Servidor MCP", "MCP server", "MCP 服务器", "MCP-Server", "Máy chủ MCP", "MCP sunucusu", "Сервер MCP", "Servidor MCP", "Server MCP", "Server MCP"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.3.16",
3
+ "version": "0.3.18",
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",