@danhachuel/thunderbolt 0.4.48 → 0.4.50
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 +120 -5
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -1913,6 +1913,70 @@ def render_tiktok_channels():
|
|
|
1913
1913
|
update_channel(channel_id, {"platform": "tiktok", "default_prompt_master": selected_prompt, "prompt_master": selected_prompt, "video_aspect_ratio": "Portrait 9:16", "style_wide": "portrait"})
|
|
1914
1914
|
st.success("Prompt Master do canal guardado.")
|
|
1915
1915
|
st.rerun()
|
|
1916
|
+
actions = st.columns([1.0, 1.0, 1.0, 1.0])
|
|
1917
|
+
with actions[0]:
|
|
1918
|
+
active = st.toggle("Activo", value=bool(channel.get("active", True)), key=f"tiktok_import_card_active_{channel_id}")
|
|
1919
|
+
if active != bool(channel.get("active", True)):
|
|
1920
|
+
update_channel(channel_id, {"active": active})
|
|
1921
|
+
st.rerun()
|
|
1922
|
+
with actions[1]:
|
|
1923
|
+
if st.button("Editar", key=f"tiktok_import_card_edit_{channel_id}", use_container_width=True):
|
|
1924
|
+
st.session_state[f"tiktok_edit_{channel_id}"] = not st.session_state.get(f"tiktok_edit_{channel_id}", False)
|
|
1925
|
+
st.rerun()
|
|
1926
|
+
with actions[2]:
|
|
1927
|
+
if st.button("Editar nicho", key=f"tiktok_import_card_niche_{channel_id}", use_container_width=True):
|
|
1928
|
+
st.session_state[f"tiktok_niche_{channel_id}"] = not st.session_state.get(f"tiktok_niche_{channel_id}", False)
|
|
1929
|
+
st.rerun()
|
|
1930
|
+
with actions[3]:
|
|
1931
|
+
if st.button("Apagar card", key=f"tiktok_import_card_delete_{channel_id}", use_container_width=True):
|
|
1932
|
+
st.session_state[f"tiktok_delete_{channel_id}"] = True
|
|
1933
|
+
st.rerun()
|
|
1934
|
+
if st.session_state.get(f"tiktok_delete_{channel_id}"):
|
|
1935
|
+
st.warning("Apagar este canal TikTok e os dados locais associados?")
|
|
1936
|
+
confirm_cols = st.columns(2)
|
|
1937
|
+
with confirm_cols[0]:
|
|
1938
|
+
if st.button("Confirmar apagar", type="primary", key=f"tiktok_confirm_delete_{channel_id}"):
|
|
1939
|
+
delete_channel(channel_id)
|
|
1940
|
+
st.session_state.pop(f"tiktok_delete_{channel_id}", None)
|
|
1941
|
+
st.rerun()
|
|
1942
|
+
with confirm_cols[1]:
|
|
1943
|
+
if st.button("Cancelar", key=f"tiktok_cancel_delete_{channel_id}"):
|
|
1944
|
+
st.session_state.pop(f"tiktok_delete_{channel_id}", None)
|
|
1945
|
+
st.rerun()
|
|
1946
|
+
if st.session_state.get(f"tiktok_niche_{channel_id}"):
|
|
1947
|
+
with st.form(f"tiktok_niche_form_{channel_id}"):
|
|
1948
|
+
niche_value = st.text_input("Nicho do canal", value=str(channel.get("niche") or ""))
|
|
1949
|
+
if st.form_submit_button("Guardar nicho", type="primary"):
|
|
1950
|
+
update_channel(channel_id, {"niche": niche_value.strip(), "reference_channels": [niche_value.strip()] if niche_value.strip() else []})
|
|
1951
|
+
st.session_state.pop(f"tiktok_niche_{channel_id}", None)
|
|
1952
|
+
st.rerun()
|
|
1953
|
+
if st.session_state.get(f"tiktok_edit_{channel_id}"):
|
|
1954
|
+
with st.form(f"tiktok_edit_form_{channel_id}"):
|
|
1955
|
+
edit_name = st.text_input("Nome do canal", value=str(channel.get("name") or ""))
|
|
1956
|
+
edit_url = st.text_input("URL pública", value=str(channel.get("url") or ""))
|
|
1957
|
+
edit_handle = st.text_input("Handle", value=str(channel.get("handle") or ""))
|
|
1958
|
+
edit_language = st.selectbox("Idioma do canal", list(LANGUAGE_CODES), index=list(LANGUAGE_CODES).index(language_code(channel.get("language") or "pt")), format_func=language_label)
|
|
1959
|
+
edit_description = st.text_area("Descrição", value=str(channel.get("description") or ""))
|
|
1960
|
+
if st.form_submit_button("Guardar edição", type="primary"):
|
|
1961
|
+
update_channel(channel_id, {"name": edit_name.strip(), "url": edit_url.strip(), "handle": edit_handle.strip(), "language": edit_language, "description": edit_description.strip(), "platform": "tiktok"})
|
|
1962
|
+
st.session_state.pop(f"tiktok_edit_{channel_id}", None)
|
|
1963
|
+
st.rerun()
|
|
1964
|
+
with st.expander("Últimos 10 vídeos publicados", expanded=False):
|
|
1965
|
+
recent_videos = channel_videos_for(channel, limit=10)
|
|
1966
|
+
if not recent_videos:
|
|
1967
|
+
st.info("Ainda não existem vídeos públicos sincronizados para este canal TikTok.")
|
|
1968
|
+
for recent_video in recent_videos[:10]:
|
|
1969
|
+
video_cols = st.columns([0.7, 3.5, 1.2])
|
|
1970
|
+
with video_cols[0]:
|
|
1971
|
+
if recent_video.get("thumbnail_url"):
|
|
1972
|
+
st.image(recent_video["thumbnail_url"], width=64)
|
|
1973
|
+
else:
|
|
1974
|
+
st.markdown("### TT")
|
|
1975
|
+
with video_cols[1]:
|
|
1976
|
+
st.write(f"**{recent_video.get('title') or 'Vídeo sem título'}**")
|
|
1977
|
+
st.caption(f"{recent_video.get('published_at') or 'Sem data'} · {recent_video.get('url') or 'Sem URL'}")
|
|
1978
|
+
with video_cols[2]:
|
|
1979
|
+
st.caption(str(recent_video.get("status") or "publicado").title())
|
|
1916
1980
|
else:
|
|
1917
1981
|
st.info("Ainda não existem canais TikTok cadastrados. Use o campo abaixo para pesquisar e cadastrar um perfil público.")
|
|
1918
1982
|
st.divider()
|
|
@@ -6749,7 +6813,7 @@ def render_settings():
|
|
|
6749
6813
|
key=f"settings_{key}",
|
|
6750
6814
|
)
|
|
6751
6815
|
|
|
6752
|
-
api_keys_tab, google_accounts_tab, tiktok_api_tab, bilibili_api_tab, ai_influencers_tab, voice_test_tab = render_localized_tabs(["API Keys", "Contas Google", "API Tiktok", "API Bilibili", "AI Influencers", "Teste de Voz"])
|
|
6816
|
+
api_keys_tab, upload_api_keys_tab, google_accounts_tab, tiktok_api_tab, bilibili_api_tab, ai_influencers_tab, voice_test_tab = render_localized_tabs(["API Keys", "API Keys Upload", "Contas Google", "API Tiktok", "API Bilibili", "AI Influencers", "Teste de Voz"])
|
|
6753
6817
|
|
|
6754
6818
|
with api_keys_tab:
|
|
6755
6819
|
with st.container(border=True):
|
|
@@ -6938,7 +7002,7 @@ def render_settings():
|
|
|
6938
7002
|
persist_callback=save_google_lyria,
|
|
6939
7003
|
)
|
|
6940
7004
|
|
|
6941
|
-
with st.expander("
|
|
7005
|
+
with st.expander("Upload-Post", expanded=False):
|
|
6942
7006
|
upload_post_enabled = st.checkbox("Activar Upload-Post", bool(settings.get("upload_post_enabled", False)))
|
|
6943
7007
|
upload_post_api_key = text_setting("Upload-Post API key", "upload_post_api_key", secret=True)
|
|
6944
7008
|
_render_credential_status(upload_post_api_key)
|
|
@@ -6952,7 +7016,7 @@ def render_settings():
|
|
|
6952
7016
|
widget_key="api_test_upload_post",
|
|
6953
7017
|
)
|
|
6954
7018
|
|
|
6955
|
-
with st.expander("Postiz
|
|
7019
|
+
with st.expander("Postiz", expanded=False):
|
|
6956
7020
|
st.caption("O Thunderbolt é o cliente. A API key é enviada exclusivamente ao servidor Postiz configurado; não é colocada em URLs, logs ou repositório.")
|
|
6957
7021
|
postiz_enabled = st.checkbox("Activar Postiz como fallback final", bool(settings.get("postiz_enabled", False)))
|
|
6958
7022
|
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.")
|
|
@@ -7003,14 +7067,65 @@ def render_settings():
|
|
|
7003
7067
|
st.success("Configurações guardadas localmente. Indique uma pasta válida do motor de vídeo para sincronizar config.toml.")
|
|
7004
7068
|
except Exception as exc:
|
|
7005
7069
|
st.warning(f"Configurações locais guardadas, mas não foi possível sincronizar config.toml: {exc}")
|
|
7070
|
+
with upload_api_keys_tab:
|
|
7071
|
+
st.subheader("API Keys Upload")
|
|
7072
|
+
st.caption("Credenciais e integrações utilizadas pelos fluxos de publicação. Cada grupo fica separado para evitar misturar chaves de geração com chaves de upload.")
|
|
7073
|
+
with st.expander("Contas Google", expanded=False):
|
|
7074
|
+
google_accounts = [item for item in settings.get("youtube_batch_accounts", []) if isinstance(item, dict) and item.get("id")]
|
|
7075
|
+
if google_accounts:
|
|
7076
|
+
for account in google_accounts:
|
|
7077
|
+
st.markdown(f"**{account.get('label') or 'Conta Google'}** — {account.get('email') or 'sem e-mail'}")
|
|
7078
|
+
st.info("Para editar documentos de cookies, OAuth e canais associados, abra a subaba Contas Google.")
|
|
7079
|
+
else:
|
|
7080
|
+
st.info("Ainda não existem Contas Google configuradas.")
|
|
7081
|
+
with st.expander("API Innertube", expanded=False):
|
|
7082
|
+
st.caption("INNERTUBE_API_KEY global usada pelo Upload directo para todas as contas Google/YouTube.")
|
|
7083
|
+
upload_innertube = st.text_input("INNERTUBE_API_KEY", value=str(settings.get("direct_innertube_api_key") or settings.get("INNERTUBE_API_KEY") or ""), type="password", key="upload_api_innertube_key")
|
|
7084
|
+
if st.button("Guardar INNERTUBE_API_KEY", type="primary", use_container_width=True, key="upload_save_innertube"):
|
|
7085
|
+
settings["direct_innertube_api_key"] = upload_innertube.strip()
|
|
7086
|
+
settings.pop("INNERTUBE_API_KEY", None)
|
|
7087
|
+
write_json("settings.json", settings)
|
|
7088
|
+
st.success("INNERTUBE_API_KEY guardada.")
|
|
7089
|
+
st.rerun()
|
|
7090
|
+
with st.expander("API Tiktok", expanded=False):
|
|
7091
|
+
render_tiktok_api_cards(settings)
|
|
7092
|
+
with st.expander("API Bilibili", expanded=False):
|
|
7093
|
+
render_bilibili_api_cards(settings)
|
|
7094
|
+
with st.expander("Composio", expanded=False):
|
|
7095
|
+
st.info("Secção reservada para uma futura integração Composio.")
|
|
7096
|
+
with st.expander("Upload-Post", expanded=False):
|
|
7097
|
+
upload_post_enabled_upload = st.checkbox("Activar Upload-Post", bool(settings.get("upload_post_enabled", False)), key="upload_tab_upload_post_enabled")
|
|
7098
|
+
upload_post_key_upload = st.text_input("Upload-Post API key", value=str(settings.get("upload_post_api_key") or ""), type="password", key="upload_tab_upload_post_key")
|
|
7099
|
+
upload_post_user_upload = st.text_input("Upload-Post username", value=str(settings.get("upload_post_username") or ""), key="upload_tab_upload_post_user")
|
|
7100
|
+
upload_post_platforms_upload = st.text_input("Plataformas Upload-Post", value=str(settings.get("upload_post_platforms") or "youtube,tiktok"), key="upload_tab_upload_post_platforms")
|
|
7101
|
+
upload_post_auto_upload = st.checkbox("Publicar automaticamente após gerar", bool(settings.get("upload_post_auto_upload", False)), key="upload_tab_upload_post_auto")
|
|
7102
|
+
if st.button("Guardar Upload-Post", type="primary", use_container_width=True, key="upload_tab_save_upload_post"):
|
|
7103
|
+
settings.update({"upload_post_enabled": bool(upload_post_enabled_upload), "upload_post_api_key": upload_post_key_upload.strip(), "upload_post_username": upload_post_user_upload.strip(), "upload_post_platforms": upload_post_platforms_upload.strip(), "upload_post_auto_upload": bool(upload_post_auto_upload)})
|
|
7104
|
+
write_json("settings.json", settings)
|
|
7105
|
+
st.success("Upload-Post guardado.")
|
|
7106
|
+
st.rerun()
|
|
7107
|
+
with st.expander("Postiz", expanded=False):
|
|
7108
|
+
postiz_enabled_upload = st.checkbox("Activar Postiz como fallback final", bool(settings.get("postiz_enabled", False)), key="upload_tab_postiz_enabled")
|
|
7109
|
+
postiz_key_upload = st.text_input("Postiz API key", value=str(settings.get("postiz_api_key") or ""), type="password", key="upload_tab_postiz_key")
|
|
7110
|
+
postiz_base_upload = st.text_input("Postiz Public API Base URL", value=str(settings.get("postiz_base_url") or "https://api.postiz.com/public/v1"), key="upload_tab_postiz_base")
|
|
7111
|
+
postiz_mcp_upload = st.text_input("Postiz MCP URL", value=str(settings.get("postiz_mcp_url") or "https://api.postiz.com/mcp"), key="upload_tab_postiz_mcp")
|
|
7112
|
+
postiz_integration_upload = st.text_input("Postiz integração padrão", value=str(settings.get("postiz_integration_id") or ""), key="upload_tab_postiz_integration")
|
|
7113
|
+
postiz_auto_upload = st.checkbox("Permitir publicação imediata no Postiz", bool(settings.get("postiz_auto_publish", False)), key="upload_tab_postiz_auto")
|
|
7114
|
+
if st.button("Guardar Postiz", type="primary", use_container_width=True, key="upload_tab_save_postiz"):
|
|
7115
|
+
settings.update({"postiz_enabled": bool(postiz_enabled_upload), "postiz_api_key": postiz_key_upload.strip(), "postiz_base_url": postiz_base_upload.strip(), "postiz_mcp_url": postiz_mcp_upload.strip(), "postiz_integration_id": postiz_integration_upload.strip(), "postiz_auto_publish": bool(postiz_auto_upload)})
|
|
7116
|
+
write_json("settings.json", settings)
|
|
7117
|
+
st.success("Postiz guardado.")
|
|
7118
|
+
st.rerun()
|
|
7006
7119
|
with google_accounts_tab:
|
|
7007
7120
|
render_google_accounts()
|
|
7008
7121
|
|
|
7009
7122
|
with tiktok_api_tab:
|
|
7010
|
-
|
|
7123
|
+
st.subheader("API Tiktok")
|
|
7124
|
+
st.info("A configuração desta API foi reorganizada em Configuração API > API Keys Upload > API Tiktok.")
|
|
7011
7125
|
|
|
7012
7126
|
with bilibili_api_tab:
|
|
7013
|
-
|
|
7127
|
+
st.subheader("API Bilibili")
|
|
7128
|
+
st.info("A configuração desta API foi reorganizada em Configuração API > API Keys Upload > API Bilibili.")
|
|
7014
7129
|
with ai_influencers_tab:
|
|
7015
7130
|
st.subheader("AI Influencers")
|
|
7016
7131
|
st.caption("Estado do backend usado por Personagens e Geração de Conteúdo IA. O selector e as credenciais são editados nesta aba, em Banco de Dados Influencers.")
|
package/package.json
CHANGED