@danhachuel/thunderbolt 0.7.56 → 0.7.57

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
@@ -95,7 +95,7 @@ from app.modules.token_optimizer.compressor import check_installation
95
95
  from app.modules.token_optimizer.config import DEFAULTS as TOKEN_OPTIMIZER_DEFAULTS
96
96
  from app.modules.token_optimizer.metrics import get_stats as get_token_optimizer_stats
97
97
  from app.influencers_ui import render_ai_influencer_characters, render_ai_influencer_content, render_ai_influencers_api_status, render_motion_control, render_ugc_products
98
- from app.social_networks_ui import render_meta_api_cards, render_social_networks
98
+ from app.social_networks_ui import render_instagram_automation, render_meta_api_cards, render_social_networks
99
99
  from hermes_ui.blueprints import create_blueprint_from_link, list_branding_files, save_generated_blueprint
100
100
  from hermes_ui.thumbnail_blueprints import generate_thumbnail_blueprint, list_thumbnail_blueprint_documents, resolve_thumbnail_blueprint, save_thumbnail_blueprint, save_thumbnail_blueprint_pair, save_thumbnail_blueprint_pairs, thumbnail_blueprint_associations, thumbnail_blueprint_catalog, thumbnail_blueprint_for_blueprint, thumbnail_blueprint_for_channel
101
101
  from hermes_ui.metadata_cleaner import build_description, clean_video_metadata, list_edit_records, metadata_manifest, normalize_tags, save_edit_record, store_external_video
@@ -2161,6 +2161,31 @@ def render_youtube_brandings():
2161
2161
  st.error(str(exc))
2162
2162
 
2163
2163
 
2164
+ def render_facebook_blueprints():
2165
+ root = STORAGE / "facebook" / "blueprints"
2166
+ root.mkdir(parents=True, exist_ok=True)
2167
+ st.title("Facebook Blueprint")
2168
+ st.caption(f"Biblioteca de modelos para conteúdos Facebook e Instagram · armazenamento em `{root}`")
2169
+ with st.form("facebook_blueprint_upload_form"):
2170
+ uploaded = st.file_uploader("Adicionar Facebook Blueprint", type=["md"], key="facebook_blueprint_upload")
2171
+ submitted = st.form_submit_button("Guardar Facebook Blueprint", type="primary")
2172
+ if submitted:
2173
+ if uploaded is None:
2174
+ st.error("Seleccione um ficheiro Markdown antes de guardar.")
2175
+ else:
2176
+ target = root / Path(uploaded.name).name
2177
+ target.write_bytes(uploaded.getvalue())
2178
+ st.success(f"Facebook Blueprint guardado: {target.name}")
2179
+ st.rerun()
2180
+ files = sorted(root.glob("*.md"))
2181
+ if not files:
2182
+ st.info("Ainda não existem Facebook Blueprints. Adicione um ficheiro Markdown para o disponibilizar nas contas Instagram.")
2183
+ for path in files:
2184
+ with st.expander(path.stem, expanded=False):
2185
+ st.caption(f"Ficheiro: `{path.name}`")
2186
+ st.code(path.read_text(encoding="utf-8"), language="markdown")
2187
+
2188
+
2164
2189
  def render_thumbnail_blueprints():
2165
2190
  st.title("Thumbnail Blueprints")
2166
2191
  st.caption(f"Documentos de estilo visual por nicho · armazenamento em `{BLUEPRINTS / 'thumbnails'}`")
@@ -10008,11 +10033,14 @@ def main():
10008
10033
  ("Canais Tiktok", ":material/music_video:", "Canais Tiktok"),
10009
10034
  ("Contas Instagram", ":material/share:", "Contas Instagram"),
10010
10035
  ("Facebook Pages", ":material/public:", "Facebook Pages"),
10036
+ ("Contas TikTok", ":material/account_circle:", "Contas TikTok"),
10037
+ ]
10038
+ blueprint_items = [
10011
10039
  ("Blueprints Youtube", ":material/library_books:", "Blueprints Youtube"),
10012
10040
  ("Thumbnail Blueprints", ":material/image:", "Thumbnail Blueprints"),
10013
10041
  ("Brandings Youtube", ":material/brush:", "Brandings Youtube"),
10014
- ("Contas TikTok", ":material/account_circle:", "Contas TikTok"),
10015
- ("Prompt Masters", ":material/auto_awesome:", "Prompt Masters"),
10042
+ ("Prompt-Masters Tiktok", ":material/auto_awesome:", "Prompt-Masters Tiktok"),
10043
+ ("Facebook Blueprint", ":material/public:", "Facebook Blueprint"),
10016
10044
  ]
10017
10045
  music_items = [
10018
10046
  ("Criação de Músicas", ":material/music_note:", "Criação de Músicas"),
@@ -10055,6 +10083,7 @@ def main():
10055
10083
  automation_items = [
10056
10084
  ("Automação Youtube", ":material/schedule:", "Automação Youtube"),
10057
10085
  ("Automação Tiktok", ":material/schedule:", "Automação Tiktok"),
10086
+ ("Automação Instagram", ":material/schedule:", "Automação Instagram"),
10058
10087
  ("Automação Facebook", ":material/schedule:", "Automação Facebook"),
10059
10088
  ("Automação Musicas", ":material/schedule:", "Automação Musicas"),
10060
10089
  ("Automação UGC", ":material/schedule:", "Automação UGC"),
@@ -10075,6 +10104,7 @@ def main():
10075
10104
  ("Pipeline Vídeos", ":material/account_tree:", "Pipeline Vídeos"),
10076
10105
  ("Pipeline Música", ":material/music_note:", "Pipeline Música"),
10077
10106
  ("AI Influencers", ":material/smart_toy:", "AI Influencers"),
10107
+ ("Blueprints", ":material/library_books:", "Blueprints"),
10078
10108
  ("Edição", ":material/edit:", "Edição"),
10079
10109
  ("Growth", ":material/analytics:", "Growth"),
10080
10110
  ("Documentação", ":material/menu_book:", "Documentação"),
@@ -10085,6 +10115,7 @@ def main():
10085
10115
  "Niche Finder": niche_finder_items,
10086
10116
  "Pipeline Vídeos": pipeline_video_items,
10087
10117
  "AI Influencers": models_ai_items,
10118
+ "Blueprints": blueprint_items,
10088
10119
  "Canais/Perfis (Vídeos)": channel_profile_items,
10089
10120
  "Pipeline Música": music_items,
10090
10121
  "Edição": edition_items,
@@ -10093,11 +10124,11 @@ def main():
10093
10124
  "Configurações": settings_items,
10094
10125
  }
10095
10126
  nav_paths = {
10096
- "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",
10127
+ "Início": "/inicio", "Automação": "/automacao", "Automação Youtube": "/automacao/youtube", "Automação Tiktok": "/automacao/tiktok", "Automação Instagram": "/automacao/instagram", "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",
10097
10128
  "Niche Finder": "/niche-finder", "Niche Finder Kaggle": "/niche-finder/kaggle", "Niche Finder Apify": "/niche-finder/apify",
10098
10129
  "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",
10099
10130
  "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",
10100
- "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",
10131
+ "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": "/blueprints/youtube", "Thumbnail Blueprints": "/blueprints/thumbnails", "Brandings Youtube": "/blueprints/brandings-youtube", "Contas TikTok": "/canais-perfis-videos/contas-tiktok", "Prompt-Masters Tiktok": "/blueprints/prompt-masters-tiktok", "Facebook Blueprint": "/blueprints/facebook",
10101
10132
  "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",
10102
10133
  "Edição": "/edicao", "Limpador de Metadados": "/edicao/limpador-metadados", "Cortes": "/edicao/cortes", "Editor Python": "/edicao/editor-python", "Download Mídia": "/edicao/download-midia",
10103
10134
  "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",
@@ -10114,10 +10145,10 @@ def main():
10114
10145
  "Música": "Pipeline Música",
10115
10146
  "Pipeline TikTok": "Canais/Perfis (Vídeos)",
10116
10147
  "Canais e Perfis de Vídeos": "Canais/Perfis (Vídeos)",
10117
- "Prompts Master": "Prompt Masters",
10148
+ "Prompts Master": "Prompt-Masters Tiktok",
10149
+ "Prompt Masters": "Prompt-Masters Tiktok",
10118
10150
  "Canais": "Canais YouTube",
10119
10151
  "Canais Youtube": "Canais YouTube",
10120
- "Blueprints": "Blueprints Youtube",
10121
10152
  "Configurações Técnicas": "Configuração API",
10122
10153
  "Models AI": "AI Influencers",
10123
10154
  "Contas Google/YouTube — canais em lote": "Configuração API",
@@ -10186,13 +10217,15 @@ def main():
10186
10217
  "Blueprints Youtube": render_blueprints,
10187
10218
  "Thumbnail Blueprints": render_thumbnail_blueprints,
10188
10219
  "Brandings Youtube": render_youtube_brandings,
10189
- "Prompt Masters": render_tiktok_prompt_masters,
10220
+ "Prompt-Masters Tiktok": render_tiktok_prompt_masters,
10221
+ "Facebook Blueprint": render_facebook_blueprints,
10190
10222
  "Canais YouTube": render_channels,
10191
10223
  "Canais Tiktok": render_tiktok_channels,
10192
10224
  "Contas TikTok": render_tiktok_accounts,
10193
10225
  "Facebook Pages": render_facebook_pages,
10194
10226
  "Automação Youtube": render_automation,
10195
10227
  "Automação Tiktok": render_tiktok_automation,
10228
+ "Automação Instagram": lambda: render_instagram_automation(read_json("settings.json", {})),
10196
10229
  "Automação Facebook": render_facebook_automation,
10197
10230
  "Automação Musicas": lambda: None,
10198
10231
  "Automação UGC": lambda: None,
@@ -5,6 +5,7 @@ import re
5
5
  import uuid
6
6
  import zipfile
7
7
  import unicodedata
8
+ from pathlib import Path
8
9
  from typing import Any, Callable, Mapping
9
10
 
10
11
  import requests
@@ -12,7 +13,7 @@ import streamlit as st
12
13
 
13
14
  from hermes_ui.domain import create_channel, delete_channel, update_channel
14
15
  from hermes_ui.influencers import InfluencerBackendError, STANDALONE_CONTENT_INFLUENCER_ID, get_repository
15
- from hermes_ui.storage import read_json, write_json
16
+ from hermes_ui.storage import STORAGE, TIKTOK_PROMPT_MASTERS, read_json, write_json
16
17
  from hermes_ui.countries import COUNTRY_OPTIONS
17
18
  from hermes_ui.languages import LANGUAGE_CODES, language_code, language_label
18
19
  from integrations.instagram_public import fetch_public_instagram_posts, fetch_public_instagram_profile, normalize_instagram_metric
@@ -32,6 +33,61 @@ def _metric(value: Any) -> str:
32
33
  return _clean(value) or "—"
33
34
 
34
35
 
36
+ INSTAGRAM_MODEL_OPTIONS = (
37
+ "Prompt-Masters Tiktok",
38
+ "Facebook Blueprint",
39
+ "Personagens Influencer/UGC",
40
+ )
41
+
42
+
43
+ def _instagram_model_options(settings: Mapping[str, Any], model_type: str) -> tuple[list[str], dict[str, str]]:
44
+ """Return the dependent model list for an Instagram account."""
45
+ if model_type == "Prompt-Masters Tiktok":
46
+ files = sorted(TIKTOK_PROMPT_MASTERS.glob("*.md")) if TIKTOK_PROMPT_MASTERS.is_dir() else []
47
+ values = [path.name for path in files]
48
+ return values, {path.name: path.stem for path in files}
49
+ if model_type == "Facebook Blueprint":
50
+ root = STORAGE / "facebook" / "blueprints"
51
+ files = sorted(root.glob("*.md")) if root.is_dir() else []
52
+ values = [path.name for path in files]
53
+ return values, {path.name: path.stem for path in files}
54
+ characters, _ = _characters(settings)
55
+ values = [_clean(item.get("id")) for item in characters if _clean(item.get("id"))]
56
+ return values, {_clean(item.get("id")): _clean(item.get("name")) or _clean(item.get("id")) for item in characters if _clean(item.get("id"))}
57
+
58
+
59
+ def resolve_instagram_model(profile: Mapping[str, Any], settings: Mapping[str, Any]) -> dict[str, str]:
60
+ """Resolve the model attached to an Instagram profile for automation."""
61
+ model_type = _clean(profile.get("instagram_model_type") or profile.get("model_type")) or INSTAGRAM_MODEL_OPTIONS[2]
62
+ if model_type not in INSTAGRAM_MODEL_OPTIONS:
63
+ model_type = INSTAGRAM_MODEL_OPTIONS[2]
64
+ model_id = _clean(profile.get("instagram_model_id") or profile.get("model_id"))
65
+ options, labels = _instagram_model_options(settings, model_type)
66
+ if model_id and model_id not in options:
67
+ model_id = ""
68
+ return {"type": model_type, "id": model_id, "label": labels.get(model_id, model_id or "Sem modelo seleccionado")}
69
+
70
+
71
+ def _render_instagram_model_selector(profile: Mapping[str, Any], settings: Mapping[str, Any], *, key_prefix: str) -> tuple[str, str]:
72
+ current = resolve_instagram_model(profile, settings)
73
+ model_type = st.selectbox(
74
+ "Atrelar modelo",
75
+ list(INSTAGRAM_MODEL_OPTIONS),
76
+ index=list(INSTAGRAM_MODEL_OPTIONS).index(current["type"]),
77
+ key=f"{key_prefix}_model_type",
78
+ help="A conta pode usar Prompt-Masters Tiktok, Facebook Blueprint ou Personagens Influencer/UGC.",
79
+ )
80
+ options, labels = _instagram_model_options(settings, model_type)
81
+ model_id = st.selectbox(
82
+ "Modelo/Blueprint",
83
+ [""] + options,
84
+ index=options.index(current["id"]) + 1 if current["id"] in options else 0,
85
+ format_func=lambda value: "Sem modelo seleccionado" if not value else labels.get(value, value),
86
+ key=f"{key_prefix}_model_id",
87
+ )
88
+ return model_type, model_id
89
+
90
+
35
91
  def _profile_metric(profile: Mapping[str, Any], *keys: str) -> Any:
36
92
  for key in keys:
37
93
  value = profile.get(key)
@@ -442,7 +498,7 @@ def _canonical_instagram_profile(data: Mapping[str, Any], existing: Mapping[str,
442
498
  return merged
443
499
 
444
500
 
445
- def _save_public_profile(data: Mapping[str, Any], *, country: str, language: str, character_id: str = "") -> dict[str, Any]:
501
+ def _save_public_profile(data: Mapping[str, Any], *, country: str, language: str, character_id: str = "", instagram_model_type: str = "", instagram_model_id: str = "") -> dict[str, Any]:
446
502
  canonical = _canonical_instagram_profile(data)
447
503
  name = _clean(canonical.get("name")) or _clean(canonical.get("username")) or "Conta Instagram"
448
504
  url = _clean(canonical.get("url"))
@@ -463,6 +519,8 @@ def _save_public_profile(data: Mapping[str, Any], *, country: str, language: str
463
519
  "subscriber_count": metrics["subscriber_count"],
464
520
  "following_count": metrics["following_count"],
465
521
  "character_id": character_id.strip(),
522
+ "instagram_model_type": instagram_model_type.strip(),
523
+ "instagram_model_id": instagram_model_id.strip(),
466
524
  "active": True,
467
525
  "metrics_source": "instagram_public_page",
468
526
  }
@@ -601,6 +659,13 @@ def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, A
601
659
  update_channel(profile_id, {"character_id": selected_character})
602
660
  st.success("Personagem associado à conta Instagram.")
603
661
  st.rerun()
662
+ model_type, model_id = _render_instagram_model_selector(profile, settings, key_prefix=f"instagram_{profile_id}")
663
+ if st.button("Guardar modelo atrelado", type="primary", key=f"save_instagram_model_{profile_id}", width="stretch"):
664
+ update_channel(profile_id, {"instagram_model_type": model_type, "instagram_model_id": model_id})
665
+ st.success(f"{model_type} atrelado à conta Instagram.")
666
+ st.rerun()
667
+ resolved_model = resolve_instagram_model(profile, settings)
668
+ st.caption(f"Modelo da Automação Instagram: {resolved_model['type']} · {resolved_model['label']}")
604
669
  _render_instagram_posts(profile)
605
670
 
606
671
 
@@ -661,12 +726,13 @@ def render_social_networks(settings: dict[str, Any]) -> None:
661
726
  character_options = [""] + [_clean(item.get("id")) for item in characters if _clean(item.get("id"))]
662
727
  character_labels = {"": "Sem personagem associado"} | {_clean(item.get("id")): _clean(item.get("name")) or _clean(item.get("id")) for item in characters}
663
728
  selected_character = st.selectbox("Personagem", character_options, format_func=lambda value: character_labels.get(value, value), key=f"social_instagram_result_character_{result_widget_id}")
729
+ selected_model_type, selected_model_id = _render_instagram_model_selector(data, settings, key_prefix=f"social_instagram_result_{result_widget_id}")
664
730
  save_profile = st.form_submit_button("Cadastrar conta Instagram", type="primary", width="stretch")
665
731
  if save_profile:
666
732
  profile_data = dict(data)
667
733
  profile_data.update({"name": name, "bio": bio, "bio_raw": bio, "post_count": _metric_input(posts), "subscriber_count": _metric_input(followers), "following_count": _metric_input(following)})
668
734
  profile_data["country"] = _normalise_country(country)
669
- _save_public_profile(profile_data, country=profile_data["country"], language=language, character_id=selected_character)
735
+ _save_public_profile(profile_data, country=profile_data["country"], language=language, character_id=selected_character, instagram_model_type=selected_model_type, instagram_model_id=selected_model_id)
670
736
  st.success("Conta Instagram cadastrada em Contas Instagram.")
671
737
  for key in ("social_instagram_result", "social_instagram_ok", "social_instagram_message"):
672
738
  st.session_state.pop(key, None)
@@ -678,3 +744,25 @@ def render_social_networks(settings: dict[str, Any]) -> None:
678
744
  characters, _ = _characters(settings)
679
745
  for profile in profiles:
680
746
  _render_instagram_card(profile, characters, settings)
747
+
748
+
749
+ def render_instagram_automation(settings: dict[str, Any]) -> None:
750
+ """Show the Instagram automation model selected on each account."""
751
+ st.title("Automação Instagram")
752
+ st.caption("Cada geração de conteúdo Instagram usa o modelo atrelado à conta: Prompt-Masters Tiktok, Facebook Blueprint ou Personagens Influencer/UGC.")
753
+ profiles = _instagram_profiles()
754
+ if not profiles:
755
+ st.info("Ainda não existem contas Instagram cadastradas. Pode continuar a trabalhar e associar um modelo quando cadastrar uma conta.")
756
+ return
757
+ for profile in profiles:
758
+ profile_id = _clean(profile.get("id"))
759
+ resolved = resolve_instagram_model(profile, settings)
760
+ with st.container(border=True):
761
+ st.subheader(_clean(profile.get("name")) or "Conta Instagram")
762
+ st.caption(_clean(profile.get("handle")) or _clean(profile.get("url")) or "sem handle")
763
+ st.write(f"**Modelo usado na geração:** {resolved['type']}")
764
+ st.write(f"**Blueprint/modelo atrelado:** {resolved['label']}")
765
+ if not resolved["id"]:
766
+ st.warning("Nenhum modelo está atrelado. Escolha-o em Contas Instagram para activar a geração desta conta.")
767
+ else:
768
+ st.success("A automação utilizará este modelo para gerar o conteúdo da conta.")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.7.56",
3
+ "version": "0.7.57",
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",