@danhachuel/thunderbolt 0.7.29 → 0.7.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/app/main.py +128 -10
  2. package/package.json +1 -1
package/app/main.py CHANGED
@@ -111,6 +111,7 @@ from hermes_ui.notifications import clear_notifications, list_notifications, mar
111
111
  from hermes_ui.influencers import BACKEND_OPTIONS, DOCUMENT_EXTENSIONS, IMAGE_EXTENSIONS, backend_name, backend_status, get_repository, test_backend
112
112
  from hermes_ui.logs import list_logs, logs_to_rows
113
113
  from hermes_ui.languages import LANGUAGE_CODES, VIDEO_LANGUAGE_CODES, LANGUAGE_FLAG_DATA_URIS, language_code, language_label, ui_language_menu_label, ui_text, video_language_label, video_language_options
114
+ from hermes_ui.countries import COUNTRY_OPTIONS
114
115
  from hermes_ui.api_key_tests import test_apify_credentials, test_influencer_database, test_innertube_api_key, test_kaggle_credentials, test_material_source_credentials, test_media_provider_card, test_nano_banana_credentials, test_postiz_credentials, test_telegram_credentials, test_tiktok_credentials, test_upload_post_credentials, test_voice_provider
115
116
  from hermes_ui.tutorials import tutorial_body, tutorial_caption, tutorial_title
116
117
  from hermes_ui.update_manager import check_version, restart_current_process, update_to_latest
@@ -1558,6 +1559,9 @@ def render_channel_edit_form(channel: dict, youtube_account_ids: list[str], yout
1558
1559
  edited_name = st.text_input("Nome do canal", value=str(channel.get("name") or ""))
1559
1560
  edited_url = st.text_input("URL", value=str(channel.get("url") or ""))
1560
1561
  edited_handle = st.text_input("Handle", value=str(channel.get("handle") or ""))
1562
+ country_options = [""] + list(COUNTRY_OPTIONS)
1563
+ current_country = str(channel.get("country") or "")
1564
+ edited_country = st.selectbox("País", country_options, index=country_options.index(current_country) if current_country in country_options else 0)
1561
1565
  language_options = VIDEO_LANGUAGE_SELECTION_OPTIONS
1562
1566
  edited_language = st.selectbox("Idioma do roteiro", language_options, index=language_options.index(normalize_video_language(channel.get("language") or "pt")) if normalize_video_language(channel.get("language") or "pt") in language_options else 0, format_func=video_language_label)
1563
1567
  edited_style = st.selectbox("Fonte do vídeo", WIDE_STYLE_OPTIONS, index=WIDE_STYLE_OPTIONS.index(channel_video_source_value(channel.get("style_wide"))) if channel_video_source_value(channel.get("style_wide")) in WIDE_STYLE_OPTIONS else 0)
@@ -1588,7 +1592,7 @@ def render_channel_edit_form(channel: dict, youtube_account_ids: list[str], yout
1588
1592
  update_channel(channel_id, {
1589
1593
  "name": edited_name.strip(), "url": edited_url.strip(), "handle": edited_handle.strip(), "language": edited_language,
1590
1594
  "style_wide": channel_video_source_storage(edited_style), "video_aspect_ratio": edited_aspect,
1591
- "niche": edited_niche.strip(), "reference_channels": [item.strip() for item in re.split(r"[,|]", edited_niche) if item.strip()],
1595
+ "niche": edited_niche.strip(), "reference_channels": [item.strip() for item in re.split(r"[,|]", edited_niche) if item.strip()], "country": edited_country.strip(),
1592
1596
  "default_blueprint_id": edited_blueprint.strip(), "blueprint_id": edited_blueprint.strip(),
1593
1597
  "default_voice": edited_voice.strip(), "voice": edited_voice.strip(),
1594
1598
  "google_account_id": edited_account.strip(), "google_account_email": str(youtube_accounts_by_id.get(edited_account, {}).get("email", "")),
@@ -2381,6 +2385,8 @@ def classify_channel_platform(channel: Any) -> str:
2381
2385
  return "tiktok"
2382
2386
  if value in {"instagram", "ig"}:
2383
2387
  return "instagram"
2388
+ if value in {"facebook", "facebook_page", "facebook_pages", "fb"}:
2389
+ return "facebook"
2384
2390
  def has_tiktok_marker(item: Any) -> bool:
2385
2391
  if isinstance(item, dict):
2386
2392
  return any(has_tiktok_marker(key) or has_tiktok_marker(value) for key, value in item.items())
@@ -2778,6 +2784,103 @@ def _tiktok_channel_url(channel: dict[str, Any]) -> str:
2778
2784
  return source if source.startswith(("http://", "https://")) else ""
2779
2785
 
2780
2786
 
2787
+ def render_facebook_pages():
2788
+ st.title("Facebook Pages")
2789
+ st.caption("Cadastre páginas públicas do Facebook por URL. Esta área aceita apenas páginas Facebook e não perfis pessoais.")
2790
+ with st.form("facebook_page_form"):
2791
+ form_cols = st.columns(2)
2792
+ with form_cols[0]:
2793
+ name = st.text_input("Nome da página", key="facebook_page_name")
2794
+ url = st.text_input("URL pública da página", placeholder="https://www.facebook.com/suapagina", key="facebook_page_url")
2795
+ country_options = [""] + list(COUNTRY_OPTIONS)
2796
+ country = st.selectbox("País", country_options, key="facebook_page_country")
2797
+ with form_cols[1]:
2798
+ description = st.text_area("Descrição", key="facebook_page_description")
2799
+ niche = st.text_input("Nicho", key="facebook_page_niche")
2800
+ active = st.toggle("Activo", value=True, key="facebook_page_active")
2801
+ submitted = st.form_submit_button("Guardar página Facebook", type="primary", width="stretch")
2802
+ if submitted:
2803
+ normalized_url = url.strip()
2804
+ if not name.strip():
2805
+ st.error("Informe o nome da página.")
2806
+ elif not normalized_url.startswith(("https://www.facebook.com/", "https://facebook.com/", "http://www.facebook.com/", "http://facebook.com/")):
2807
+ st.error("Informe uma URL pública válida de uma página Facebook.")
2808
+ else:
2809
+ existing = [item for item in read_json("channels.json", []) if isinstance(item, dict)]
2810
+ duplicate = next((item for item in existing if classify_channel_platform(item) == "facebook" and str(item.get("url") or "").strip().casefold() == normalized_url.casefold()), None)
2811
+ if duplicate:
2812
+ st.warning("Esta página Facebook já está cadastrada.")
2813
+ else:
2814
+ page = create_channel(name.strip(), normalized_url, {
2815
+ "platform": "facebook",
2816
+ "social_network": "Facebook Pages",
2817
+ "country": country.strip(),
2818
+ "description": description.strip(),
2819
+ "niche": niche.strip(),
2820
+ "active": bool(active),
2821
+ "metrics_source": "public_url",
2822
+ })
2823
+ update_channel(page["id"], {"platform": "facebook", "social_network": "Facebook Pages"})
2824
+ st.success(f"Página {page['name']} guardada.")
2825
+ st.rerun()
2826
+
2827
+ pages = [item for item in read_json("channels.json", []) if isinstance(item, dict) and classify_channel_platform(item) == "facebook"]
2828
+ st.divider()
2829
+ st.subheader(f"Facebook Pages cadastradas ({len(pages)})")
2830
+ if not pages:
2831
+ st.info("Nenhuma página Facebook cadastrada.")
2832
+ return
2833
+ for page in pages:
2834
+ page_id = str(page.get("id") or "")
2835
+ with st.container(border=True):
2836
+ header_cols = st.columns([0.7, 2.75, 1.15, 1.1, 1.0, 1.35, 1.35])
2837
+ with header_cols[0]:
2838
+ st.markdown("### FB")
2839
+ with header_cols[1]:
2840
+ page_country = str(page.get("country") or "").strip()
2841
+ st.write(f"**{page.get('name', 'Sem nome')}**{f' · {page_country}' if page_country else ''}")
2842
+ st.caption(page.get("niche") or "SEM NICHO CONFIGURADO")
2843
+ st.caption(page.get("url") or "sem URL")
2844
+ with header_cols[2]:
2845
+ if page.get("url"):
2846
+ st.link_button("Abrir página", str(page["url"]), type="primary", width="content")
2847
+ with header_cols[3]:
2848
+ st.metric("País", page_country or "—")
2849
+ with header_cols[4]:
2850
+ st.metric("Estado", "Activo" if page.get("active", True) else "Inactivo")
2851
+ with header_cols[5]:
2852
+ st.caption("Facebook Page")
2853
+ with header_cols[6]:
2854
+ active_value = st.toggle("Activo", value=bool(page.get("active", True)), key=f"facebook_active_{page_id}")
2855
+ if active_value != bool(page.get("active", True)):
2856
+ update_channel(page_id, {"active": active_value})
2857
+ st.rerun()
2858
+ with st.expander("Detalhes e configuração da página", expanded=False):
2859
+ with st.form(f"facebook_page_edit_{page_id}"):
2860
+ edited_name = st.text_input("Nome da página", value=str(page.get("name") or ""))
2861
+ edited_url = st.text_input("URL pública", value=str(page.get("url") or ""))
2862
+ edit_country_options = [""] + list(COUNTRY_OPTIONS)
2863
+ current_page_country = str(page.get("country") or "")
2864
+ edited_country = st.selectbox("País", edit_country_options, index=edit_country_options.index(current_page_country) if current_page_country in edit_country_options else 0)
2865
+ edited_description = st.text_area("Descrição", value=str(page.get("description") or ""))
2866
+ edited_niche = st.text_input("Nicho", value=str(page.get("niche") or ""))
2867
+ save_page = st.form_submit_button("Guardar alterações", type="primary")
2868
+ action_cols = st.columns(2)
2869
+ with action_cols[0]:
2870
+ if save_page:
2871
+ if not edited_name.strip() or not edited_url.strip().startswith(("https://www.facebook.com/", "https://facebook.com/", "http://www.facebook.com/", "http://facebook.com/")):
2872
+ st.error("Informe o nome e uma URL pública válida de uma página Facebook.")
2873
+ else:
2874
+ update_channel(page_id, {"name": edited_name.strip(), "url": edited_url.strip(), "country": edited_country.strip(), "description": edited_description.strip(), "niche": edited_niche.strip()})
2875
+ st.success("Página Facebook actualizada.")
2876
+ st.rerun()
2877
+ with action_cols[1]:
2878
+ if st.button("Apagar", key=f"facebook_delete_{page_id}"):
2879
+ delete_channel(page_id)
2880
+ st.success("Página Facebook apagada.")
2881
+ st.rerun()
2882
+
2883
+
2781
2884
  def render_channels():
2782
2885
  st.title("Canais Youtube")
2783
2886
  st.caption("Escolha entre importar dados públicos do YouTube ou preencher o canal manualmente.")
@@ -2849,6 +2952,8 @@ def render_channels():
2849
2952
  url = st.text_input("URL", value=imported.get("url", source if source.startswith("http") else ""), key="yt_import_url")
2850
2953
  handle = st.text_input("Handle", value=imported.get("handle", ""), key="yt_import_handle")
2851
2954
  language = st.selectbox("Idioma do roteiro", VIDEO_LANGUAGE_SELECTION_OPTIONS, index=VIDEO_LANGUAGE_SELECTION_OPTIONS.index(normalize_video_language(imported.get("language") or "pt")) if normalize_video_language(imported.get("language") or "pt") in VIDEO_LANGUAGE_SELECTION_OPTIONS else 0, format_func=video_language_label, key="yt_import_language")
2955
+ import_country_options = [""] + list(COUNTRY_OPTIONS)
2956
+ import_country = st.selectbox("País", import_country_options, index=import_country_options.index(str(imported.get("country") or "")) if str(imported.get("country") or "") in import_country_options else 0, key="yt_import_country")
2852
2957
  style = st.selectbox("Fonte do vídeo", WIDE_STYLE_OPTIONS, index=0, key="yt_import_style")
2853
2958
  video_aspect_ratio = st.selectbox("Proporção do vídeo", CHANNEL_ASPECT_RATIO_OPTIONS, key="yt_import_aspect_ratio")
2854
2959
  blueprint = st.selectbox("Blueprint Padrão", blueprint_ids, index=blueprint_ids.index(imported_blueprint) if imported_blueprint in blueprint_ids else 0, format_func=lambda item: blueprint_labels.get(item, item or "Sem Blueprint padrão"), key="yt_import_blueprint")
@@ -2879,7 +2984,7 @@ def render_channels():
2879
2984
  "description": description.strip(),
2880
2985
  "niche": niche.strip(),
2881
2986
  "reference_channels": [item.strip() for item in re.split(r"[,|]", niche) if item.strip()],
2882
- "language": language,
2987
+ "language": language, "country": import_country.strip(),
2883
2988
  "style_wide": channel_video_source_storage(style), "video_aspect_ratio": video_aspect_ratio, "format": "wide",
2884
2989
  "blueprint_id": blueprint.strip(),
2885
2990
  "default_blueprint_id": blueprint.strip(),
@@ -3138,6 +3243,8 @@ def render_channels():
3138
3243
  description = st.text_area("Descrição", key="manual_channel_description")
3139
3244
  niche = st.text_input("Nicho", placeholder="Ex.: História militar, mistérios, ciência", key="manual_channel_niche")
3140
3245
  language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=list(LANGUAGE_CODES).index("pt"), format_func=language_label, key="manual_channel_language")
3246
+ manual_country_options = [""] + list(COUNTRY_OPTIONS)
3247
+ manual_country = st.selectbox("País", manual_country_options, key="manual_channel_country")
3141
3248
  style = st.selectbox("Estilo wide", ["Pexels/Pixabay", "full_ia", "Apenas Música"], index=0, key="manual_channel_style")
3142
3249
  manual_blueprint_items = blueprint_catalog()
3143
3250
  manual_blueprint_ids = [item[0] for item in manual_blueprint_items]
@@ -3166,7 +3273,7 @@ def render_channels():
3166
3273
  "description": description.strip(),
3167
3274
  "niche": niche.strip(),
3168
3275
  "reference_channels": [item.strip() for item in re.split(r"[,|]", niche) if item.strip()],
3169
- "language": language,
3276
+ "language": language, "country": manual_country.strip(),
3170
3277
  "style_wide": {"Pexels/Pixabay": "pexels", "full_ia": "full_ia", "Apenas Música": "music"}.get(style, style),
3171
3278
  "blueprint_id": blueprint.strip(),
3172
3279
  "default_blueprint_id": blueprint.strip(),
@@ -3228,7 +3335,8 @@ def render_channels():
3228
3335
  else:
3229
3336
  st.markdown("### YT")
3230
3337
  with header_cols[1]:
3231
- st.write(f"**{channel.get('name', 'Sem nome')}**")
3338
+ country_label = str(channel.get("country") or "").strip()
3339
+ st.write(f"**{channel.get('name', 'Sem nome')}**{f' · {country_label}' if country_label else ''}")
3232
3340
  st.caption(channel_niche_label(channel))
3233
3341
  st.caption(f"{channel.get('handle') or channel.get('url') or 'sem URL'} · {channel.get('metrics_source', 'manual')}")
3234
3342
  with header_cols[2]:
@@ -9552,12 +9660,13 @@ def main():
9552
9660
  channel_profile_items = [
9553
9661
  ("Canais YouTube", ":material/ondemand_video:", "Canais YouTube"),
9554
9662
  ("Canais Tiktok", ":material/music_video:", "Canais Tiktok"),
9663
+ ("Contas Instagram", ":material/share:", "Contas Instagram"),
9664
+ ("Facebook Pages", ":material/public:", "Facebook Pages"),
9555
9665
  ("Blueprints Youtube", ":material/library_books:", "Blueprints Youtube"),
9556
9666
  ("Thumbnail Blueprints", ":material/image:", "Thumbnail Blueprints"),
9557
9667
  ("Brandings Youtube", ":material/brush:", "Brandings Youtube"),
9558
9668
  ("Contas TikTok", ":material/account_circle:", "Contas TikTok"),
9559
9669
  ("Prompt Masters", ":material/auto_awesome:", "Prompt Masters"),
9560
- ("Facebook Pages", ":material/public:", "Facebook Pages"),
9561
9670
  ]
9562
9671
  music_items = [
9563
9672
  ("Criação de Músicas", ":material/music_note:", "Criação de Músicas"),
@@ -9570,7 +9679,6 @@ def main():
9570
9679
  ("Geração de Conteúdo IA", ":material/auto_awesome:", "Geração de Conteúdo IA"),
9571
9680
  ("Motion Control", ":material/motion_photos_on:", "Motion Control"),
9572
9681
  ("UGC Products", ":material/shopping_bag:", "UGC Products"),
9573
- ("Contas Instagram", ":material/share:", "Contas Instagram"),
9574
9682
  ]
9575
9683
  growth_items = [
9576
9684
  ("Analista Growth Youtube", ":material/analytics:", "Analista Growth Youtube"),
@@ -9601,6 +9709,11 @@ def main():
9601
9709
  automation_items = [
9602
9710
  ("Automação Youtube", ":material/schedule:", "Automação Youtube"),
9603
9711
  ("Automação Tiktok", ":material/schedule:", "Automação Tiktok"),
9712
+ ("Automação Facebook", ":material/schedule:", "Automação Facebook"),
9713
+ ("Automação Musicas", ":material/schedule:", "Automação Musicas"),
9714
+ ("Automação UGC", ":material/schedule:", "Automação UGC"),
9715
+ ("Automação Influencer Content", ":material/schedule:", "Automação Influencer Content"),
9716
+ ("Automação Bilibili", ":material/schedule:", "Automação Bilibili"),
9604
9717
  ]
9605
9718
  edition_items = [
9606
9719
  ("Limpador de Metadados", ":material/edit_note:", "Limpador de Metadados"),
@@ -9634,12 +9747,12 @@ def main():
9634
9747
  "Configurações": settings_items,
9635
9748
  }
9636
9749
  nav_paths = {
9637
- "Início": "/inicio", "Automação": "/automacao", "Automação Youtube": "/automacao/youtube", "Automação Tiktok": "/automacao/tiktok",
9750
+ "Início": "/inicio", "Automação": "/automacao", "Automação Youtube": "/automacao/youtube", "Automação Tiktok": "/automacao/tiktok", "Automação Facebook": "/automacao/facebook", "Automação Musicas": "/automacao/musicas", "Automação UGC": "/automacao/ugc", "Automação Influencer Content": "/automacao/influencer-content", "Automação Bilibili": "/automacao/bilibili",
9638
9751
  "Niche Finder": "/niche-finder", "Niche Finder Kaggle": "/niche-finder/kaggle", "Niche Finder Apify": "/niche-finder/apify",
9639
9752
  "Pipeline Vídeos": "/pipeline-videos", "Criação de Vídeos": "/pipeline-videos/criacao", "Criação de Shorts": "/pipeline-videos/shorts", "Backlog Vídeos": "/pipeline-videos/backlog", "Roteiros": "/pipeline-videos/roteiros", "Thumbnails": "/pipeline-videos/thumbnails", "Upload": "/pipeline-videos/upload", "Update Youtube Vídeos": "/pipeline-videos/update-youtube",
9640
9753
  "Pipeline Música": "/pipeline-musica", "Criação de Músicas": "/pipeline-musica/criacao", "Music Backlog": "/pipeline-musica/backlog", "Vozes Personalizadas": "/pipeline-musica/vozes-personalizadas", "Upload Música": "/pipeline-musica/upload",
9641
- "Canais/Perfis (Vídeos)": "/canais-perfis-videos", "Canais YouTube": "/canais-perfis-videos/canais-youtube", "Canais Tiktok": "/canais-perfis-videos/canais-tiktok", "Blueprints Youtube": "/canais-perfis-videos/blueprints-youtube", "Thumbnail Blueprints": "/canais-perfis-videos/thumbnail-blueprints", "Brandings Youtube": "/canais-perfis-videos/brandings-youtube", "Contas TikTok": "/canais-perfis-videos/contas-tiktok", "Prompt Masters": "/canais-perfis-videos/prompt-masters", "Facebook Pages": "/canais-perfis-videos/facebook-pages",
9642
- "AI Influencers": "/ai-influencers", "Personagens": "/ai-influencers/personagens", "Geração de Conteúdo IA": "/ai-influencers/geracao-conteudo", "Motion Control": "/ai-influencers/motion-control", "UGC Products": "/ai-influencers/ugc-products", "Contas Instagram": "/ai-influencers/redes-sociais",
9754
+ "Canais/Perfis (Vídeos)": "/canais-perfis-videos", "Canais YouTube": "/canais-perfis-videos/canais-youtube", "Canais Tiktok": "/canais-perfis-videos/canais-tiktok", "Contas Instagram": "/canais-perfis-videos/contas-instagram", "Facebook Pages": "/canais-perfis-videos/facebook-pages", "Blueprints Youtube": "/canais-perfis-videos/blueprints-youtube", "Thumbnail Blueprints": "/canais-perfis-videos/thumbnail-blueprints", "Brandings Youtube": "/canais-perfis-videos/brandings-youtube", "Contas TikTok": "/canais-perfis-videos/contas-tiktok", "Prompt Masters": "/canais-perfis-videos/prompt-masters",
9755
+ "AI Influencers": "/ai-influencers", "Personagens": "/ai-influencers/personagens", "Geração de Conteúdo IA": "/ai-influencers/geracao-conteudo", "Motion Control": "/ai-influencers/motion-control", "UGC Products": "/ai-influencers/ugc-products",
9643
9756
  "Edição": "/edicao", "Limpador de Metadados": "/edicao/limpador-metadados", "Cortes": "/edicao/cortes", "Editor Python": "/edicao/editor-python", "Download Mídia": "/edicao/download-midia",
9644
9757
  "Growth": "/growth", "Analista Growth Youtube": "/growth/youtube", "Analista Growth Tiktok": "/growth/tiktok", "Analista Growth Instagram": "/growth/instagram", "Analista Facebook Pages": "/growth/facebook-pages", "Analista Bilibili": "/growth/bilibili",
9645
9758
  "Documentação": "/documentacao", "Tutorial Meta": "/documentacao/meta", "Tutorial Supabase": "/documentacao/supabase", "Tutorial Kaggle": "/documentacao/kaggle", "Tutorial Apify": "/documentacao/apify", "Tutorial YouTube Video-Upload Frontend": "/documentacao/youtube-video-upload-frontend", "Tutorial OAuth do Google": "/documentacao/oauth-google", "Tutorial YouTube Data API Key (Public Data)": "/documentacao/youtube-data-api-key",
@@ -9731,9 +9844,14 @@ def main():
9731
9844
  "Canais YouTube": render_channels,
9732
9845
  "Canais Tiktok": render_tiktok_channels,
9733
9846
  "Contas TikTok": render_tiktok_accounts,
9734
- "Facebook Pages": lambda: render_edit_placeholder("Facebook Pages", ""),
9847
+ "Facebook Pages": render_facebook_pages,
9735
9848
  "Automação Youtube": render_automation,
9736
9849
  "Automação Tiktok": render_tiktok_automation,
9850
+ "Automação Facebook": lambda: None,
9851
+ "Automação Musicas": lambda: None,
9852
+ "Automação UGC": lambda: None,
9853
+ "Automação Influencer Content": lambda: None,
9854
+ "Automação Bilibili": lambda: None,
9737
9855
  "Niche Finder Kaggle": render_niche_finder,
9738
9856
  "Tutorial Kaggle": lambda: render_niche_tutorial("kaggle"),
9739
9857
  "Niche Finder Apify": render_niche_finder_apify,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.7.29",
3
+ "version": "0.7.30",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",