@danhachuel/thunderbolt 0.3.15 → 0.3.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/app/main.py +148 -139
  2. package/package.json +1 -1
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))
@@ -3929,136 +3936,138 @@ def render_settings():
3929
3936
  with api_keys_tab:
3930
3937
  api_service_tab, material_sources_tab = render_localized_tabs(["Serviços e modelos", "Fontes de materiais"])
3931
3938
  with api_service_tab:
3932
- st.subheader("API Keys")
3933
- port = st.number_input("Porta Streamlit", 1, 65535, int(settings.get("port", 3030)))
3934
- moneyprinter_path = st.text_input("Pasta do motor de vídeo", settings.get("moneyprinter_path", ""), key="settings_moneyprinter_path")
3935
- render_llm_provider_cards(settings)
3936
- with st.form("settings_form"):
3937
- with st.expander("Niche Finder — Kaggle", expanded=False):
3938
- 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.")
3939
- kaggle_cols = st.columns(3)
3940
- with kaggle_cols[0]:
3941
- kaggle_username = text_setting("Kaggle Username", "kaggle_username", help_text="Nome de utilizador da sua conta Kaggle, sem @ e sem URL.")
3942
- with kaggle_cols[1]:
3943
- 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.")
3944
- with kaggle_cols[2]:
3945
- kaggle_kernel_slug = text_setting("Slug da kernel", "kaggle_kernel_slug", help_text="Identificador da kernel remota, por exemplo thunderbolt-niche-finder.")
3946
- _render_credential_status(kaggle_api_key)
3947
-
3948
- with st.expander("Niche Finder — Apify", expanded=False):
3949
- 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.")
3950
- apify_cols = st.columns(4)
3951
- with apify_cols[0]:
3952
- 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.")
3953
- with apify_cols[1]:
3954
- apify_actor_id = text_setting("Apify Actor ID", "apify_actor_id", help_text="Por padrão: streamers~youtube-scraper.")
3955
- with apify_cols[2]:
3956
- 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)
3957
- with apify_cols[3]:
3958
- 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)
3959
- _render_credential_status(apify_api_token)
3960
-
3961
- with st.expander("Nano Banana — geração de thumbnails", expanded=False):
3962
- 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.")
3963
- nano_cols = st.columns(2)
3964
- with nano_cols[0]:
3965
- 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.")
3966
- 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)
3967
- with nano_cols[1]:
3968
- 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)
3969
- 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)
3970
- _render_credential_status(gemini_image_api_key)
3971
-
3972
-
3973
- with st.expander("Voz, TTS e música — Azure Speech, restantes serviços e Suno", expanded=False):
3974
- cols = st.columns(2)
3975
- with cols[0]:
3976
- azure_speech_key = text_setting("Azure Speech key", "azure_speech_key", secret=True)
3977
- _render_credential_status(azure_speech_key)
3978
- azure_speech_region = text_setting("Azure Speech region", "azure_speech_region")
3979
- siliconflow_tts_api_key = text_setting("SiliconFlow TTS API key", "siliconflow_tts_api_key", secret=True)
3980
- _render_credential_status(siliconflow_tts_api_key)
3981
- minimax_tts_api_key = text_setting("MiniMax TTS API key", "minimax_tts_api_key", secret=True)
3982
- _render_credential_status(minimax_tts_api_key)
3983
- minimax_tts_base_url = text_setting("MiniMax TTS Base URL", "minimax_tts_base_url")
3984
- minimax_tts_model_id = text_setting("MiniMax TTS model", "minimax_tts_model_id")
3985
- minimax_tts_voice_id = text_setting("MiniMax TTS voice ID", "minimax_tts_voice_id")
3986
- with cols[1]:
3987
- elevenlabs_api_key = text_setting("ElevenLabs API key", "elevenlabs_api_key", secret=True)
3988
- _render_credential_status(elevenlabs_api_key)
3989
- elevenlabs_model_id = text_setting("ElevenLabs model", "elevenlabs_model_id")
3990
- chatterbox_base_url = text_setting("Chatterbox Base URL", "chatterbox_base_url")
3991
- chatterbox_api_key = text_setting("Chatterbox API key", "chatterbox_api_key", secret=True)
3992
- _render_credential_status(chatterbox_api_key, local=True, required=False)
3993
- chatterbox_model_id = text_setting("Chatterbox model", "chatterbox_model_id")
3994
- sonilo_api_key = text_setting("Sonilo API key", "sonilo_api_key", secret=True)
3995
- _render_credential_status(sonilo_api_key)
3996
- sonilo_base_url = text_setting("Sonilo Base URL", "sonilo_base_url")
3997
- st.markdown("**Suno — agente musical opcional**")
3998
- suno_api_key = text_setting("Suno API key", "suno_api_key", secret=True)
3999
- _render_credential_status(suno_api_key)
4000
- 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.")
4001
- suno_api_endpoint = text_setting("Suno API endpoint", "suno_api_endpoint", help_text="Ex.: /api/generate")
4002
-
4003
- with st.expander("TikTok for Developers — Client ID e Client Secret", expanded=False):
4004
- 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.")
4005
- tiktok_client_key = text_setting("TikTok Client ID", "tiktok_client_key", secret=True)
4006
- _render_credential_status(tiktok_client_key)
4007
- tiktok_client_secret = text_setting("TikTok Client Secret", "tiktok_client_secret", secret=True)
4008
-
4009
- with st.expander("Publicação através do Upload-Post", expanded=False):
4010
- upload_post_enabled = st.checkbox("Activar Upload-Post", bool(settings.get("upload_post_enabled", False)))
4011
- upload_post_api_key = text_setting("Upload-Post API key", "upload_post_api_key", secret=True)
4012
- _render_credential_status(upload_post_api_key)
4013
- upload_post_username = text_setting("Upload-Post username", "upload_post_username")
4014
- upload_post_platforms = text_setting("Plataformas Upload-Post", "upload_post_platforms")
4015
- upload_post_auto_upload = st.checkbox("Publicar automaticamente após gerar", bool(settings.get("upload_post_auto_upload", False)))
4016
-
4017
- with st.expander("Postiz API key, integração e MCP", expanded=False):
4018
- st.caption("O Thunderbolt é o cliente. A API key é enviada exclusivamente ao servidor Postiz configurado; não é colocada em URLs, logs ou repositório.")
4019
- postiz_enabled = st.checkbox("Activar Postiz como fallback final", bool(settings.get("postiz_enabled", False)))
4020
- 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.")
4021
- postiz_cols = st.columns(2)
4022
- with postiz_cols[0]:
4023
- 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.")
4024
- _render_credential_status(postiz_api_key)
4025
- 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")
4026
- postiz_integration_id = text_setting("Postiz integração padrão", "postiz_integration_id", help_text="ID do canal/integração devolvido por GET /integrations.")
4027
- with postiz_cols[1]:
4028
- 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.")
4029
- postiz_auto_publish = st.checkbox("Permitir publicação imediata no Postiz", bool(settings.get("postiz_auto_publish", False)))
4030
- 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.")
4031
-
4032
- save_all_settings = st.form_submit_button("Guardar configurações do Thunderbolt", type="primary")
4033
- if save_all_settings:
4034
- settings.update({
4035
- "port": port, "moneyprinter_path": moneyprinter_path,
4036
- "kaggle_username": kaggle_username.strip(), "kaggle_api_key": kaggle_api_key.strip(), "kaggle_kernel_slug": kaggle_kernel_slug.strip() or "thunderbolt-niche-finder",
4037
- "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),
4038
- "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,
4039
- "azure_speech_key": azure_speech_key, "azure_speech_region": azure_speech_region,
4040
- "siliconflow_tts_api_key": siliconflow_tts_api_key, "minimax_tts_api_key": minimax_tts_api_key,
4041
- "minimax_tts_base_url": minimax_tts_base_url, "minimax_tts_model_id": minimax_tts_model_id, "minimax_tts_voice_id": minimax_tts_voice_id,
4042
- "elevenlabs_api_key": elevenlabs_api_key, "elevenlabs_model_id": elevenlabs_model_id,
4043
- "chatterbox_base_url": chatterbox_base_url, "chatterbox_api_key": chatterbox_api_key, "chatterbox_model_id": chatterbox_model_id,
4044
- "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,
4045
- "tiktok_client_key": tiktok_client_key, "tiktok_client_secret": tiktok_client_secret,
4046
- "upload_post_enabled": upload_post_enabled, "upload_post_api_key": upload_post_api_key,
4047
- "upload_post_username": upload_post_username, "upload_post_platforms": upload_post_platforms,
4048
- "upload_post_auto_upload": upload_post_auto_upload,
4049
- "postiz_enabled": postiz_enabled, "postiz_api_key": postiz_api_key, "postiz_base_url": postiz_base_url.strip() or "https://api.postiz.com/public/v1",
4050
- "postiz_mcp_url": postiz_mcp_url.strip() or "https://api.postiz.com/mcp", "postiz_mode": postiz_mode,
4051
- "postiz_integration_id": postiz_integration_id.strip(), "postiz_auto_publish": bool(postiz_auto_publish),
4052
- })
4053
- write_json("settings.json", settings)
4054
- try:
4055
- synced = sync_moneyprinter_config(settings, moneyprinter_path)
4056
- if synced:
4057
- st.success(f"Configurações guardadas e sincronizadas com {synced}")
4058
- else:
4059
- st.success("Configurações guardadas localmente. Indique uma pasta válida do motor de vídeo para sincronizar config.toml.")
4060
- except Exception as exc:
4061
- st.warning(f"Configurações locais guardadas, mas não foi possível sincronizar config.toml: {exc}")
3939
+ with st.container(border=True):
3940
+ st.subheader("API Keys")
3941
+ port = st.number_input("Porta Streamlit", 1, 65535, int(settings.get("port", 3030)))
3942
+ moneyprinter_path = st.text_input("Pasta do motor de vídeo", settings.get("moneyprinter_path", ""), key="settings_moneyprinter_path")
3943
+ with st.form("settings_form"):
3944
+ with st.expander("Niche Finder — Kaggle", expanded=False):
3945
+ 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.")
3946
+ kaggle_cols = st.columns(3)
3947
+ with kaggle_cols[0]:
3948
+ kaggle_username = text_setting("Kaggle Username", "kaggle_username", help_text="Nome de utilizador da sua conta Kaggle, sem @ e sem URL.")
3949
+ with kaggle_cols[1]:
3950
+ 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.")
3951
+ with kaggle_cols[2]:
3952
+ kaggle_kernel_slug = text_setting("Slug da kernel", "kaggle_kernel_slug", help_text="Identificador da kernel remota, por exemplo thunderbolt-niche-finder.")
3953
+ _render_credential_status(kaggle_api_key)
3954
+
3955
+ with st.expander("Niche Finder — Apify", expanded=False):
3956
+ 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.")
3957
+ apify_cols = st.columns(4)
3958
+ with apify_cols[0]:
3959
+ 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.")
3960
+ with apify_cols[1]:
3961
+ apify_actor_id = text_setting("Apify Actor ID", "apify_actor_id", help_text="Por padrão: streamers~youtube-scraper.")
3962
+ with apify_cols[2]:
3963
+ 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)
3964
+ with apify_cols[3]:
3965
+ 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)
3966
+ _render_credential_status(apify_api_token)
3967
+
3968
+ render_llm_provider_cards(settings, embedded=True)
3969
+
3970
+ with st.expander("Nano Banana — geração de thumbnails", expanded=False):
3971
+ 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.")
3972
+ nano_cols = st.columns(2)
3973
+ with nano_cols[0]:
3974
+ 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.")
3975
+ 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)
3976
+ with nano_cols[1]:
3977
+ 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)
3978
+ 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)
3979
+ _render_credential_status(gemini_image_api_key)
3980
+
3981
+
3982
+ with st.expander("Voz, TTS e música — Azure Speech, restantes serviços e Suno", expanded=False):
3983
+ cols = st.columns(2)
3984
+ with cols[0]:
3985
+ azure_speech_key = text_setting("Azure Speech key", "azure_speech_key", secret=True)
3986
+ _render_credential_status(azure_speech_key)
3987
+ azure_speech_region = text_setting("Azure Speech region", "azure_speech_region")
3988
+ siliconflow_tts_api_key = text_setting("SiliconFlow TTS API key", "siliconflow_tts_api_key", secret=True)
3989
+ _render_credential_status(siliconflow_tts_api_key)
3990
+ minimax_tts_api_key = text_setting("MiniMax TTS API key", "minimax_tts_api_key", secret=True)
3991
+ _render_credential_status(minimax_tts_api_key)
3992
+ minimax_tts_base_url = text_setting("MiniMax TTS Base URL", "minimax_tts_base_url")
3993
+ minimax_tts_model_id = text_setting("MiniMax TTS model", "minimax_tts_model_id")
3994
+ minimax_tts_voice_id = text_setting("MiniMax TTS voice ID", "minimax_tts_voice_id")
3995
+ with cols[1]:
3996
+ elevenlabs_api_key = text_setting("ElevenLabs API key", "elevenlabs_api_key", secret=True)
3997
+ _render_credential_status(elevenlabs_api_key)
3998
+ elevenlabs_model_id = text_setting("ElevenLabs model", "elevenlabs_model_id")
3999
+ chatterbox_base_url = text_setting("Chatterbox Base URL", "chatterbox_base_url")
4000
+ chatterbox_api_key = text_setting("Chatterbox API key", "chatterbox_api_key", secret=True)
4001
+ _render_credential_status(chatterbox_api_key, local=True, required=False)
4002
+ chatterbox_model_id = text_setting("Chatterbox model", "chatterbox_model_id")
4003
+ sonilo_api_key = text_setting("Sonilo API key", "sonilo_api_key", secret=True)
4004
+ _render_credential_status(sonilo_api_key)
4005
+ sonilo_base_url = text_setting("Sonilo Base URL", "sonilo_base_url")
4006
+ st.markdown("**Suno — agente musical opcional**")
4007
+ suno_api_key = text_setting("Suno API key", "suno_api_key", secret=True)
4008
+ _render_credential_status(suno_api_key)
4009
+ 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.")
4010
+ suno_api_endpoint = text_setting("Suno API endpoint", "suno_api_endpoint", help_text="Ex.: /api/generate")
4011
+
4012
+ with st.expander("TikTok for Developers — Client ID e Client Secret", expanded=False):
4013
+ 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.")
4014
+ tiktok_client_key = text_setting("TikTok Client ID", "tiktok_client_key", secret=True)
4015
+ _render_credential_status(tiktok_client_key)
4016
+ tiktok_client_secret = text_setting("TikTok Client Secret", "tiktok_client_secret", secret=True)
4017
+
4018
+ with st.expander("Publicação através do Upload-Post", expanded=False):
4019
+ upload_post_enabled = st.checkbox("Activar Upload-Post", bool(settings.get("upload_post_enabled", False)))
4020
+ upload_post_api_key = text_setting("Upload-Post API key", "upload_post_api_key", secret=True)
4021
+ _render_credential_status(upload_post_api_key)
4022
+ upload_post_username = text_setting("Upload-Post username", "upload_post_username")
4023
+ upload_post_platforms = text_setting("Plataformas Upload-Post", "upload_post_platforms")
4024
+ upload_post_auto_upload = st.checkbox("Publicar automaticamente após gerar", bool(settings.get("upload_post_auto_upload", False)))
4025
+
4026
+ with st.expander("Postiz API key, integração e MCP", expanded=False):
4027
+ st.caption("O Thunderbolt é o cliente. A API key é enviada exclusivamente ao servidor Postiz configurado; não é colocada em URLs, logs ou repositório.")
4028
+ postiz_enabled = st.checkbox("Activar Postiz como fallback final", bool(settings.get("postiz_enabled", False)))
4029
+ 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.")
4030
+ postiz_cols = st.columns(2)
4031
+ with postiz_cols[0]:
4032
+ 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.")
4033
+ _render_credential_status(postiz_api_key)
4034
+ 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")
4035
+ postiz_integration_id = text_setting("Postiz integração padrão", "postiz_integration_id", help_text="ID do canal/integração devolvido por GET /integrations.")
4036
+ with postiz_cols[1]:
4037
+ 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.")
4038
+ postiz_auto_publish = st.checkbox("Permitir publicação imediata no Postiz", bool(settings.get("postiz_auto_publish", False)))
4039
+ 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.")
4040
+
4041
+ save_all_settings = st.form_submit_button("Guardar configurações do Thunderbolt", type="primary")
4042
+ if save_all_settings:
4043
+ settings.update({
4044
+ "port": port, "moneyprinter_path": moneyprinter_path,
4045
+ "kaggle_username": kaggle_username.strip(), "kaggle_api_key": kaggle_api_key.strip(), "kaggle_kernel_slug": kaggle_kernel_slug.strip() or "thunderbolt-niche-finder",
4046
+ "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),
4047
+ "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,
4048
+ "azure_speech_key": azure_speech_key, "azure_speech_region": azure_speech_region,
4049
+ "siliconflow_tts_api_key": siliconflow_tts_api_key, "minimax_tts_api_key": minimax_tts_api_key,
4050
+ "minimax_tts_base_url": minimax_tts_base_url, "minimax_tts_model_id": minimax_tts_model_id, "minimax_tts_voice_id": minimax_tts_voice_id,
4051
+ "elevenlabs_api_key": elevenlabs_api_key, "elevenlabs_model_id": elevenlabs_model_id,
4052
+ "chatterbox_base_url": chatterbox_base_url, "chatterbox_api_key": chatterbox_api_key, "chatterbox_model_id": chatterbox_model_id,
4053
+ "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,
4054
+ "tiktok_client_key": tiktok_client_key, "tiktok_client_secret": tiktok_client_secret,
4055
+ "upload_post_enabled": upload_post_enabled, "upload_post_api_key": upload_post_api_key,
4056
+ "upload_post_username": upload_post_username, "upload_post_platforms": upload_post_platforms,
4057
+ "upload_post_auto_upload": upload_post_auto_upload,
4058
+ "postiz_enabled": postiz_enabled, "postiz_api_key": postiz_api_key, "postiz_base_url": postiz_base_url.strip() or "https://api.postiz.com/public/v1",
4059
+ "postiz_mcp_url": postiz_mcp_url.strip() or "https://api.postiz.com/mcp", "postiz_mode": postiz_mode,
4060
+ "postiz_integration_id": postiz_integration_id.strip(), "postiz_auto_publish": bool(postiz_auto_publish),
4061
+ })
4062
+ write_json("settings.json", settings)
4063
+ try:
4064
+ synced = sync_moneyprinter_config(settings, moneyprinter_path)
4065
+ if synced:
4066
+ st.success(f"Configurações guardadas e sincronizadas com {synced}")
4067
+ else:
4068
+ st.success("Configurações guardadas localmente. Indique uma pasta válida do motor de vídeo para sincronizar config.toml.")
4069
+ except Exception as exc:
4070
+ st.warning(f"Configurações locais guardadas, mas não foi possível sincronizar config.toml: {exc}")
4062
4071
  with material_sources_tab:
4063
4072
  render_material_source_api_keys(settings)
4064
4073
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
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",