@danhachuel/thunderbolt 0.7.55 → 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 +104 -9
- package/app/social_networks_ui.py +91 -3
- package/package.json +1 -1
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'}`")
|
|
@@ -8811,7 +8836,7 @@ def render_settings():
|
|
|
8811
8836
|
key=f"settings_{key}",
|
|
8812
8837
|
)
|
|
8813
8838
|
|
|
8814
|
-
api_keys_tab, upload_api_keys_tab, ai_influencers_tab, voice_test_tab = render_localized_tabs(["API Keys", "API Keys Upload", "AI Influencers", "Teste de Voz"])
|
|
8839
|
+
api_keys_tab, upload_api_keys_tab, subtitles_tab, ai_influencers_tab, voice_test_tab = render_localized_tabs(["API Keys", "API Keys Upload", "Legendas", "AI Influencers", "Teste de Voz"])
|
|
8815
8840
|
|
|
8816
8841
|
with api_keys_tab:
|
|
8817
8842
|
with st.container(border=True):
|
|
@@ -9191,6 +9216,68 @@ def render_settings():
|
|
|
9191
9216
|
write_json("settings.json", settings)
|
|
9192
9217
|
st.success("Postiz guardado.")
|
|
9193
9218
|
st.rerun()
|
|
9219
|
+
with subtitles_tab:
|
|
9220
|
+
st.subheader("Legendas")
|
|
9221
|
+
st.caption("Escolha como o motor de vídeo gera as legendas. A configuração só é alterada quando clicar em guardar.")
|
|
9222
|
+
st.markdown("### Os dois modos de geração")
|
|
9223
|
+
st.markdown("O projecto oferece duas abordagens principais para criar as legendas:")
|
|
9224
|
+
st.table([
|
|
9225
|
+
{"Modo": "edge", "Velocidade": "Rápida", "Requisitos de Hardware": "Nenhum (roda em CPU)", "Qualidade": "Pode ser instável"},
|
|
9226
|
+
{"Modo": "whisper", "Velocidade": "Lenta", "Requisitos de Hardware": "Alta (GPU recomendada)", "Qualidade": "Mais confiável"},
|
|
9227
|
+
])
|
|
9228
|
+
subtitle_provider_options = ["edge", "whisper"]
|
|
9229
|
+
current_subtitle_provider = str(settings.get("subtitle_provider") or "edge").strip().lower()
|
|
9230
|
+
if current_subtitle_provider not in subtitle_provider_options:
|
|
9231
|
+
current_subtitle_provider = "edge"
|
|
9232
|
+
subtitle_provider = st.radio(
|
|
9233
|
+
"Modo de geração",
|
|
9234
|
+
subtitle_provider_options,
|
|
9235
|
+
index=subtitle_provider_options.index(current_subtitle_provider),
|
|
9236
|
+
format_func=lambda value: f"{value} — {'Rápida, CPU, pode ser instável' if value == 'edge' else 'Lenta, GPU recomendada, mais confiável'}",
|
|
9237
|
+
horizontal=True,
|
|
9238
|
+
key="subtitle_provider_ui",
|
|
9239
|
+
)
|
|
9240
|
+
st.caption("A escolha é feita alterando a propriedade `subtitle_provider` no `config.toml`.")
|
|
9241
|
+
whisper_cols = st.columns(3)
|
|
9242
|
+
with whisper_cols[0]:
|
|
9243
|
+
whisper_model_size = st.selectbox(
|
|
9244
|
+
"Modelo Whisper",
|
|
9245
|
+
["tiny", "base", "small", "medium", "large-v3"],
|
|
9246
|
+
index=["tiny", "base", "small", "medium", "large-v3"].index(str(settings.get("whisper_model_size") or "large-v3")) if str(settings.get("whisper_model_size") or "large-v3") in ["tiny", "base", "small", "medium", "large-v3"] else 4,
|
|
9247
|
+
key="subtitle_whisper_model_size",
|
|
9248
|
+
)
|
|
9249
|
+
with whisper_cols[1]:
|
|
9250
|
+
whisper_device = st.selectbox(
|
|
9251
|
+
"Dispositivo Whisper",
|
|
9252
|
+
["cpu", "cuda"],
|
|
9253
|
+
index=["cpu", "cuda"].index(str(settings.get("whisper_device") or "cpu")) if str(settings.get("whisper_device") or "cpu") in ["cpu", "cuda"] else 0,
|
|
9254
|
+
key="subtitle_whisper_device",
|
|
9255
|
+
)
|
|
9256
|
+
with whisper_cols[2]:
|
|
9257
|
+
whisper_compute_type = st.selectbox(
|
|
9258
|
+
"Tipo de cálculo Whisper",
|
|
9259
|
+
["int8", "float16", "float32"],
|
|
9260
|
+
index=["int8", "float16", "float32"].index(str(settings.get("whisper_compute_type") or "int8")) if str(settings.get("whisper_compute_type") or "int8") in ["int8", "float16", "float32"] else 0,
|
|
9261
|
+
key="subtitle_whisper_compute_type",
|
|
9262
|
+
)
|
|
9263
|
+
if st.button("Guardar configuração de legendas", type="primary", width="stretch", key="save_subtitle_settings"):
|
|
9264
|
+
settings.update({
|
|
9265
|
+
"subtitle_provider": subtitle_provider,
|
|
9266
|
+
"whisper_model_size": whisper_model_size,
|
|
9267
|
+
"whisper_device": whisper_device,
|
|
9268
|
+
"whisper_compute_type": whisper_compute_type,
|
|
9269
|
+
})
|
|
9270
|
+
write_json("settings.json", settings)
|
|
9271
|
+
subtitle_moneyprinter_path = str(settings.get("moneyprinter_path") or "").strip()
|
|
9272
|
+
if subtitle_moneyprinter_path:
|
|
9273
|
+
try:
|
|
9274
|
+
synced = sync_moneyprinter_config(settings, subtitle_moneyprinter_path)
|
|
9275
|
+
st.success(f"Configuração de legendas guardada{f' e sincronizada com {synced}' if synced else ''}.")
|
|
9276
|
+
except Exception as exc:
|
|
9277
|
+
st.warning(f"Configuração de legendas guardada localmente, mas não foi possível sincronizar config.toml: {exc}")
|
|
9278
|
+
else:
|
|
9279
|
+
st.success("Configuração de legendas guardada localmente.")
|
|
9280
|
+
|
|
9194
9281
|
with ai_influencers_tab:
|
|
9195
9282
|
st.subheader("AI Influencers")
|
|
9196
9283
|
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.")
|
|
@@ -9946,11 +10033,14 @@ def main():
|
|
|
9946
10033
|
("Canais Tiktok", ":material/music_video:", "Canais Tiktok"),
|
|
9947
10034
|
("Contas Instagram", ":material/share:", "Contas Instagram"),
|
|
9948
10035
|
("Facebook Pages", ":material/public:", "Facebook Pages"),
|
|
10036
|
+
("Contas TikTok", ":material/account_circle:", "Contas TikTok"),
|
|
10037
|
+
]
|
|
10038
|
+
blueprint_items = [
|
|
9949
10039
|
("Blueprints Youtube", ":material/library_books:", "Blueprints Youtube"),
|
|
9950
10040
|
("Thumbnail Blueprints", ":material/image:", "Thumbnail Blueprints"),
|
|
9951
10041
|
("Brandings Youtube", ":material/brush:", "Brandings Youtube"),
|
|
9952
|
-
("
|
|
9953
|
-
("
|
|
10042
|
+
("Prompt-Masters Tiktok", ":material/auto_awesome:", "Prompt-Masters Tiktok"),
|
|
10043
|
+
("Facebook Blueprint", ":material/public:", "Facebook Blueprint"),
|
|
9954
10044
|
]
|
|
9955
10045
|
music_items = [
|
|
9956
10046
|
("Criação de Músicas", ":material/music_note:", "Criação de Músicas"),
|
|
@@ -9993,6 +10083,7 @@ def main():
|
|
|
9993
10083
|
automation_items = [
|
|
9994
10084
|
("Automação Youtube", ":material/schedule:", "Automação Youtube"),
|
|
9995
10085
|
("Automação Tiktok", ":material/schedule:", "Automação Tiktok"),
|
|
10086
|
+
("Automação Instagram", ":material/schedule:", "Automação Instagram"),
|
|
9996
10087
|
("Automação Facebook", ":material/schedule:", "Automação Facebook"),
|
|
9997
10088
|
("Automação Musicas", ":material/schedule:", "Automação Musicas"),
|
|
9998
10089
|
("Automação UGC", ":material/schedule:", "Automação UGC"),
|
|
@@ -10013,6 +10104,7 @@ def main():
|
|
|
10013
10104
|
("Pipeline Vídeos", ":material/account_tree:", "Pipeline Vídeos"),
|
|
10014
10105
|
("Pipeline Música", ":material/music_note:", "Pipeline Música"),
|
|
10015
10106
|
("AI Influencers", ":material/smart_toy:", "AI Influencers"),
|
|
10107
|
+
("Blueprints", ":material/library_books:", "Blueprints"),
|
|
10016
10108
|
("Edição", ":material/edit:", "Edição"),
|
|
10017
10109
|
("Growth", ":material/analytics:", "Growth"),
|
|
10018
10110
|
("Documentação", ":material/menu_book:", "Documentação"),
|
|
@@ -10023,6 +10115,7 @@ def main():
|
|
|
10023
10115
|
"Niche Finder": niche_finder_items,
|
|
10024
10116
|
"Pipeline Vídeos": pipeline_video_items,
|
|
10025
10117
|
"AI Influencers": models_ai_items,
|
|
10118
|
+
"Blueprints": blueprint_items,
|
|
10026
10119
|
"Canais/Perfis (Vídeos)": channel_profile_items,
|
|
10027
10120
|
"Pipeline Música": music_items,
|
|
10028
10121
|
"Edição": edition_items,
|
|
@@ -10031,11 +10124,11 @@ def main():
|
|
|
10031
10124
|
"Configurações": settings_items,
|
|
10032
10125
|
}
|
|
10033
10126
|
nav_paths = {
|
|
10034
|
-
"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",
|
|
10035
10128
|
"Niche Finder": "/niche-finder", "Niche Finder Kaggle": "/niche-finder/kaggle", "Niche Finder Apify": "/niche-finder/apify",
|
|
10036
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",
|
|
10037
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",
|
|
10038
|
-
"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": "/
|
|
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",
|
|
10039
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",
|
|
10040
10133
|
"Edição": "/edicao", "Limpador de Metadados": "/edicao/limpador-metadados", "Cortes": "/edicao/cortes", "Editor Python": "/edicao/editor-python", "Download Mídia": "/edicao/download-midia",
|
|
10041
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",
|
|
@@ -10052,10 +10145,10 @@ def main():
|
|
|
10052
10145
|
"Música": "Pipeline Música",
|
|
10053
10146
|
"Pipeline TikTok": "Canais/Perfis (Vídeos)",
|
|
10054
10147
|
"Canais e Perfis de Vídeos": "Canais/Perfis (Vídeos)",
|
|
10055
|
-
"Prompts Master": "Prompt
|
|
10148
|
+
"Prompts Master": "Prompt-Masters Tiktok",
|
|
10149
|
+
"Prompt Masters": "Prompt-Masters Tiktok",
|
|
10056
10150
|
"Canais": "Canais YouTube",
|
|
10057
10151
|
"Canais Youtube": "Canais YouTube",
|
|
10058
|
-
"Blueprints": "Blueprints Youtube",
|
|
10059
10152
|
"Configurações Técnicas": "Configuração API",
|
|
10060
10153
|
"Models AI": "AI Influencers",
|
|
10061
10154
|
"Contas Google/YouTube — canais em lote": "Configuração API",
|
|
@@ -10124,13 +10217,15 @@ def main():
|
|
|
10124
10217
|
"Blueprints Youtube": render_blueprints,
|
|
10125
10218
|
"Thumbnail Blueprints": render_thumbnail_blueprints,
|
|
10126
10219
|
"Brandings Youtube": render_youtube_brandings,
|
|
10127
|
-
"Prompt
|
|
10220
|
+
"Prompt-Masters Tiktok": render_tiktok_prompt_masters,
|
|
10221
|
+
"Facebook Blueprint": render_facebook_blueprints,
|
|
10128
10222
|
"Canais YouTube": render_channels,
|
|
10129
10223
|
"Canais Tiktok": render_tiktok_channels,
|
|
10130
10224
|
"Contas TikTok": render_tiktok_accounts,
|
|
10131
10225
|
"Facebook Pages": render_facebook_pages,
|
|
10132
10226
|
"Automação Youtube": render_automation,
|
|
10133
10227
|
"Automação Tiktok": render_tiktok_automation,
|
|
10228
|
+
"Automação Instagram": lambda: render_instagram_automation(read_json("settings.json", {})),
|
|
10134
10229
|
"Automação Facebook": render_facebook_automation,
|
|
10135
10230
|
"Automação Musicas": lambda: None,
|
|
10136
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