@danhachuel/thunderbolt 0.3.14 → 0.3.16

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
@@ -3791,17 +3791,25 @@ def _render_llm_card(settings: dict[str, Any], cards: list[dict[str, Any]], inde
3791
3791
  model = st.text_input("Modelo manual", value=current_model if selected == manual_model and current_model not in model_catalog else "", key=f"llm_card_{card_id}_model_manual") if selected == manual_model else selected
3792
3792
  else:
3793
3793
  model = st.text_input("Modelo", value=current_model, help="ID do modelo usado pelo endpoint Chat Completions.", key=f"llm_card_{card_id}_model")
3794
- if definition.show_base_url:
3795
- base_url = st.text_input(
3796
- "Base URL",
3797
- value=str(card.get("base_url") or definition.default_base_url),
3798
- help="Endpoint OpenAI-compatible deste provider; só aparece quando é configurável.",
3799
- key=f"llm_card_{card_id}_base_url",
3800
- )
3801
- else:
3802
- base_url = str(card.get("base_url") or definition.default_base_url)
3803
- if base_url:
3794
+ base_url = str(card.get("base_url") or definition.default_base_url)
3795
+ endpoint_col, action_col = st.columns([1.65, 1.05])
3796
+ with endpoint_col:
3797
+ if definition.show_base_url:
3798
+ base_url = st.text_input(
3799
+ "Base URL",
3800
+ value=base_url,
3801
+ help="Endpoint OpenAI-compatible deste provider; só aparece quando é configurável.",
3802
+ key=f"llm_card_{card_id}_base_url",
3803
+ )
3804
+ elif base_url:
3804
3805
  st.caption(f"Endpoint gerido pelo provider: {base_url}")
3806
+ with action_col:
3807
+ action_buttons = st.columns(2)
3808
+ with action_buttons[0]:
3809
+ refresh_clicked = st.form_submit_button("Consultar modelos", use_container_width=True)
3810
+ with action_buttons[1]:
3811
+ test_clicked = st.form_submit_button("Testar chamada API", use_container_width=True)
3812
+
3805
3813
  extra_values: dict[str, str] = {}
3806
3814
  if definition.extra_fields:
3807
3815
  extra_cols = st.columns(len(definition.extra_fields))
@@ -3812,26 +3820,26 @@ def _render_llm_card(settings: dict[str, Any], cards: list[dict[str, Any]], inde
3812
3820
  value=str(card.get(field_name) or ""),
3813
3821
  key=f"llm_card_{card_id}_{field_name}",
3814
3822
  )
3815
- enabled = st.checkbox("Provider activo", value=bool(card.get("enabled", True)), key=f"llm_card_{card_id}_enabled")
3816
- primary_llm = st.checkbox(
3817
- "LLM principal",
3818
- value=str(settings.get(LLM_ACTIVE_CARD_KEY) or "") == card_id,
3819
- help="Usar este cartão como provider principal para geração de temas, roteiros, títulos e keywords.",
3820
- key=f"llm_card_{card_id}_primary",
3821
- )
3822
- telegram_llm = st.checkbox(
3823
- "LLM Telegram",
3824
- value=bool(card.get("telegram_llm", False)),
3825
- help="Usar este cartão exclusivamente para o roteamento de notificações Telegram. Apenas um cartão pode ficar seleccionado.",
3826
- key=f"llm_card_{card_id}_telegram",
3827
- )
3828
- action_cols = st.columns([1.35, 1.35, 1.1])
3829
- with action_cols[0]:
3830
- test_clicked = st.form_submit_button("Testar chamada API", use_container_width=True)
3831
- with action_cols[1]:
3832
- refresh_clicked = st.form_submit_button("Consultar modelos", use_container_width=True)
3833
- with action_cols[2]:
3834
- save_clicked = st.form_submit_button("Salvar", type="primary", use_container_width=True)
3823
+
3824
+ status_cols = st.columns(3)
3825
+ with status_cols[0]:
3826
+ enabled = st.checkbox("Provider activo", value=bool(card.get("enabled", True)), key=f"llm_card_{card_id}_enabled")
3827
+ with status_cols[1]:
3828
+ primary_llm = st.checkbox(
3829
+ "LLM principal",
3830
+ value=str(settings.get(LLM_ACTIVE_CARD_KEY) or "") == card_id,
3831
+ help="Usar este cartão como provider principal para geração de temas, roteiros, títulos e keywords.",
3832
+ key=f"llm_card_{card_id}_primary",
3833
+ )
3834
+ with status_cols[2]:
3835
+ telegram_llm = st.checkbox(
3836
+ "LLM Telegram",
3837
+ value=bool(card.get("telegram_llm", False)),
3838
+ help="Usar este cartão exclusivamente para o roteamento de notificações Telegram. Apenas um cartão pode ficar seleccionado.",
3839
+ key=f"llm_card_{card_id}_telegram",
3840
+ )
3841
+
3842
+ save_clicked = st.form_submit_button("Salvar", type="primary", use_container_width=True)
3835
3843
  remove_clicked = False
3836
3844
  if definition.code != "openai":
3837
3845
  remove_clicked = st.form_submit_button("Remover cartão", use_container_width=True)
@@ -3921,136 +3929,137 @@ def render_settings():
3921
3929
  with api_keys_tab:
3922
3930
  api_service_tab, material_sources_tab = render_localized_tabs(["Serviços e modelos", "Fontes de materiais"])
3923
3931
  with api_service_tab:
3924
- with st.form("settings_form"):
3932
+ with st.container(border=True):
3925
3933
  st.subheader("API Keys")
3926
3934
  port = st.number_input("Porta Streamlit", 1, 65535, int(settings.get("port", 3030)))
3927
3935
  moneyprinter_path = st.text_input("Pasta do motor de vídeo", settings.get("moneyprinter_path", ""), key="settings_moneyprinter_path")
3928
- with st.expander("Niche Finder — execução remota no Kaggle", expanded=False):
3929
- 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.")
3930
- kaggle_cols = st.columns(3)
3931
- with kaggle_cols[0]:
3932
- kaggle_username = text_setting("Kaggle Username", "kaggle_username", help_text="Nome de utilizador da sua conta Kaggle, sem @ e sem URL.")
3933
- with kaggle_cols[1]:
3934
- 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.")
3935
- with kaggle_cols[2]:
3936
- kaggle_kernel_slug = text_setting("Slug da kernel", "kaggle_kernel_slug", help_text="Identificador da kernel remota, por exemplo thunderbolt-niche-finder.")
3937
- _render_credential_status(kaggle_api_key)
3938
-
3939
- with st.expander("Niche Finder — execução através da Apify", expanded=False):
3940
- 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.")
3941
- apify_cols = st.columns(4)
3942
- with apify_cols[0]:
3943
- 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.")
3944
- with apify_cols[1]:
3945
- apify_actor_id = text_setting("Apify Actor ID", "apify_actor_id", help_text="Por padrão: streamers~youtube-scraper.")
3946
- with apify_cols[2]:
3947
- 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)
3948
- with apify_cols[3]:
3949
- 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)
3950
- _render_credential_status(apify_api_token)
3951
-
3952
- with st.expander("Nano Banana — geração de thumbnails", expanded=False):
3953
- 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.")
3954
- nano_cols = st.columns(2)
3955
- with nano_cols[0]:
3956
- 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.")
3957
- 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)
3958
- with nano_cols[1]:
3959
- 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)
3960
- 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)
3961
- _render_credential_status(gemini_image_api_key)
3962
-
3963
-
3964
- with st.expander("Voz, TTS e música — Azure Speech, restantes serviços e Suno", expanded=False):
3965
- cols = st.columns(2)
3966
- with cols[0]:
3967
- azure_speech_key = text_setting("Azure Speech key", "azure_speech_key", secret=True)
3968
- _render_credential_status(azure_speech_key)
3969
- azure_speech_region = text_setting("Azure Speech region", "azure_speech_region")
3970
- siliconflow_tts_api_key = text_setting("SiliconFlow TTS API key", "siliconflow_tts_api_key", secret=True)
3971
- _render_credential_status(siliconflow_tts_api_key)
3972
- minimax_tts_api_key = text_setting("MiniMax TTS API key", "minimax_tts_api_key", secret=True)
3973
- _render_credential_status(minimax_tts_api_key)
3974
- minimax_tts_base_url = text_setting("MiniMax TTS Base URL", "minimax_tts_base_url")
3975
- minimax_tts_model_id = text_setting("MiniMax TTS model", "minimax_tts_model_id")
3976
- minimax_tts_voice_id = text_setting("MiniMax TTS voice ID", "minimax_tts_voice_id")
3977
- with cols[1]:
3978
- elevenlabs_api_key = text_setting("ElevenLabs API key", "elevenlabs_api_key", secret=True)
3979
- _render_credential_status(elevenlabs_api_key)
3980
- elevenlabs_model_id = text_setting("ElevenLabs model", "elevenlabs_model_id")
3981
- chatterbox_base_url = text_setting("Chatterbox Base URL", "chatterbox_base_url")
3982
- chatterbox_api_key = text_setting("Chatterbox API key", "chatterbox_api_key", secret=True)
3983
- _render_credential_status(chatterbox_api_key, local=True, required=False)
3984
- chatterbox_model_id = text_setting("Chatterbox model", "chatterbox_model_id")
3985
- sonilo_api_key = text_setting("Sonilo API key", "sonilo_api_key", secret=True)
3986
- _render_credential_status(sonilo_api_key)
3987
- sonilo_base_url = text_setting("Sonilo Base URL", "sonilo_base_url")
3988
- st.markdown("**Suno — agente musical opcional**")
3989
- suno_api_key = text_setting("Suno API key", "suno_api_key", secret=True)
3990
- _render_credential_status(suno_api_key)
3991
- 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.")
3992
- suno_api_endpoint = text_setting("Suno API endpoint", "suno_api_endpoint", help_text="Ex.: /api/generate")
3993
-
3994
- with st.expander("TikTok for Developers — Client ID e Client Secret", expanded=False):
3995
- 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.")
3996
- tiktok_client_key = text_setting("TikTok Client ID", "tiktok_client_key", secret=True)
3997
- _render_credential_status(tiktok_client_key)
3998
- tiktok_client_secret = text_setting("TikTok Client Secret", "tiktok_client_secret", secret=True)
3999
-
4000
- with st.expander("Publicação através do Upload-Post", expanded=False):
4001
- upload_post_enabled = st.checkbox("Activar Upload-Post", bool(settings.get("upload_post_enabled", False)))
4002
- upload_post_api_key = text_setting("Upload-Post API key", "upload_post_api_key", secret=True)
4003
- _render_credential_status(upload_post_api_key)
4004
- upload_post_username = text_setting("Upload-Post username", "upload_post_username")
4005
- upload_post_platforms = text_setting("Plataformas Upload-Post", "upload_post_platforms")
4006
- upload_post_auto_upload = st.checkbox("Publicar automaticamente após gerar", bool(settings.get("upload_post_auto_upload", False)))
4007
-
4008
- with st.expander("Postiz API key, integração e MCP", expanded=False):
4009
- st.caption("O Thunderbolt é o cliente. A API key é enviada exclusivamente ao servidor Postiz configurado; não é colocada em URLs, logs ou repositório.")
4010
- postiz_enabled = st.checkbox("Activar Postiz como fallback final", bool(settings.get("postiz_enabled", False)))
4011
- 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.")
4012
- postiz_cols = st.columns(2)
4013
- with postiz_cols[0]:
4014
- 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.")
4015
- _render_credential_status(postiz_api_key)
4016
- 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")
4017
- postiz_integration_id = text_setting("Postiz integração padrão", "postiz_integration_id", help_text="ID do canal/integração devolvido por GET /integrations.")
4018
- with postiz_cols[1]:
4019
- 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.")
4020
- postiz_auto_publish = st.checkbox("Permitir publicação imediata no Postiz", bool(settings.get("postiz_auto_publish", False)))
4021
- st.caption("No Upload, a aba Postiz permite carregar as integrações e enviar vídeos manualmente. No fluxo recomendado, Postiz é tentado depois da API Oficial e do Upload directo.")
4022
-
4023
- save_all_settings = st.form_submit_button("Guardar configurações do Thunderbolt", type="primary")
4024
- if save_all_settings:
4025
- settings.update({
4026
- "port": port, "moneyprinter_path": moneyprinter_path,
4027
- "kaggle_username": kaggle_username.strip(), "kaggle_api_key": kaggle_api_key.strip(), "kaggle_kernel_slug": kaggle_kernel_slug.strip() or "thunderbolt-niche-finder",
4028
- "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),
4029
- "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,
4030
- "azure_speech_key": azure_speech_key, "azure_speech_region": azure_speech_region,
4031
- "siliconflow_tts_api_key": siliconflow_tts_api_key, "minimax_tts_api_key": minimax_tts_api_key,
4032
- "minimax_tts_base_url": minimax_tts_base_url, "minimax_tts_model_id": minimax_tts_model_id, "minimax_tts_voice_id": minimax_tts_voice_id,
4033
- "elevenlabs_api_key": elevenlabs_api_key, "elevenlabs_model_id": elevenlabs_model_id,
4034
- "chatterbox_base_url": chatterbox_base_url, "chatterbox_api_key": chatterbox_api_key, "chatterbox_model_id": chatterbox_model_id,
4035
- "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,
4036
- "tiktok_client_key": tiktok_client_key, "tiktok_client_secret": tiktok_client_secret,
4037
- "upload_post_enabled": upload_post_enabled, "upload_post_api_key": upload_post_api_key,
4038
- "upload_post_username": upload_post_username, "upload_post_platforms": upload_post_platforms,
4039
- "upload_post_auto_upload": upload_post_auto_upload,
4040
- "postiz_enabled": postiz_enabled, "postiz_api_key": postiz_api_key, "postiz_base_url": postiz_base_url.strip() or "https://api.postiz.com/public/v1",
4041
- "postiz_mcp_url": postiz_mcp_url.strip() or "https://api.postiz.com/mcp", "postiz_mode": postiz_mode,
4042
- "postiz_integration_id": postiz_integration_id.strip(), "postiz_auto_publish": bool(postiz_auto_publish),
4043
- })
4044
- write_json("settings.json", settings)
4045
- try:
4046
- synced = sync_moneyprinter_config(settings, moneyprinter_path)
4047
- if synced:
4048
- st.success(f"Configurações guardadas e sincronizadas com {synced}")
4049
- else:
4050
- st.success("Configurações guardadas localmente. Indique uma pasta válida do motor de vídeo para sincronizar config.toml.")
4051
- except Exception as exc:
4052
- st.warning(f"Configurações locais guardadas, mas não foi possível sincronizar config.toml: {exc}")
4053
- render_llm_provider_cards(settings)
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 só 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}")
4054
4063
  with material_sources_tab:
4055
4064
  render_material_source_api_keys(settings)
4056
4065
 
@@ -208,6 +208,22 @@ for _language_code, _llm_control_values in _LLM_CONTROL_TRANSLATIONS.items():
208
208
  UI_TRANSLATIONS[_language_code].update(_llm_control_values)
209
209
 
210
210
 
211
+ _API_KEY_EXPANDER_TRANSLATIONS: dict[str, dict[str, str]] = {
212
+ "pt": {"Niche Finder — Kaggle": "Niche Finder — Kaggle", "Niche Finder — Apify": "Niche Finder — Apify"},
213
+ "en": {"Niche Finder — Kaggle": "Niche Finder — Kaggle", "Niche Finder — Apify": "Niche Finder — Apify"},
214
+ "zh": {"Niche Finder — Kaggle": "利基搜索 — Kaggle", "Niche Finder — Apify": "利基搜索 — Apify"},
215
+ "de": {"Niche Finder — Kaggle": "Kaggle-Nischenfinder", "Niche Finder — Apify": "Apify-Nischenfinder"},
216
+ "vi": {"Niche Finder — Kaggle": "Tìm ngách — Kaggle", "Niche Finder — Apify": "Tìm ngách — Apify"},
217
+ "tr": {"Niche Finder — Kaggle": "Kaggle Niş Bulucu", "Niche Finder — Apify": "Apify Niş Bulucu"},
218
+ "ru": {"Niche Finder — Kaggle": "Поиск ниши — Kaggle", "Niche Finder — Apify": "Поиск ниши — Apify"},
219
+ "es": {"Niche Finder — Kaggle": "Buscador de nichos — Kaggle", "Niche Finder — Apify": "Buscador de nichos — Apify"},
220
+ "id": {"Niche Finder — Kaggle": "Pencari Niche — Kaggle", "Niche Finder — Apify": "Pencari Niche — Apify"},
221
+ "it": {"Niche Finder — Kaggle": "Trova nicchia — Kaggle", "Niche Finder — Apify": "Trova nicchia — Apify"},
222
+ }
223
+ for _language_code, _api_key_values in _API_KEY_EXPANDER_TRANSLATIONS.items():
224
+ UI_TRANSLATIONS[_language_code].update(_api_key_values)
225
+
226
+
211
227
  # Labels introduced by the reorganised sidebar. They are merged into the
212
228
  # existing UI translation index so every navigation control uses the same
213
229
  # translation path as the legacy pages.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
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",