@danhachuel/thunderbolt 0.3.51 → 0.3.53
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/MANUAL-INSTALACAO.md +21 -5
- package/README.md +15 -4
- package/THIRD-PARTY-NOTICES.md +4 -0
- package/app/influencers_ui.py +400 -0
- package/app/main.py +100 -10
- package/hermes_ui/api_key_tests.py +10 -0
- package/hermes_ui/influencers.py +569 -0
- package/hermes_ui/languages.py +5 -0
- package/hermes_ui/media_generation.py +75 -3
- package/hermes_ui/media_providers.py +10 -0
- package/hermes_ui/notifications.py +2 -0
- package/hermes_ui/storage.py +7 -1
- package/package.json +3 -1
- package/requirements.txt +1 -0
- package/seed/references/ai_influencers_schema.sql +69 -0
- package/seed/references/guide-supabase.md +9 -1
package/app/main.py
CHANGED
|
@@ -31,6 +31,7 @@ from app.modules.niche_finder.apify import ApifyError, DEFAULT_ACTOR_ID, abort_a
|
|
|
31
31
|
from app.modules.niche_finder.core import NicheAnalysisError, run_niche_analysis
|
|
32
32
|
from app.modules.niche_finder.data_loader import DatasetError, download_kaggle_dataset
|
|
33
33
|
from app.modules.niche_finder.summarizer import summarize_items
|
|
34
|
+
from app.influencers_ui import render_ai_influencer_characters, render_ai_influencer_content, render_ai_influencer_motion_control, render_ai_influencers_api_status
|
|
34
35
|
from hermes_ui.blueprints import create_blueprint_from_link, list_branding_files, save_generated_blueprint
|
|
35
36
|
from hermes_ui.metadata_cleaner import build_description, clean_video_metadata, list_edit_records, metadata_manifest, normalize_tags, save_edit_record, store_external_video
|
|
36
37
|
from hermes_ui.python_editor import AUDIO_EXTENSIONS, VIDEO_EXTENSIONS, PythonEditorError, change_speed, editor_manifest, extract_audio, list_edit_records as list_python_editor_records, list_generated_videos, list_scripts, list_video_files, read_script, remove_audio, replace_audio, resize_video, save_edit_record as save_python_editor_record, save_script, store_uploaded_asset, trim_video
|
|
@@ -39,13 +40,14 @@ from hermes_ui.mcp import detect_local_service, install_skill_locally, load_inte
|
|
|
39
40
|
from hermes_ui.mcp_server import server_status, start_server, stop_server
|
|
40
41
|
from hermes_ui.material_sources import apply_material_source_cards_to_settings, ensure_material_source_cards, material_source_catalog, material_source_definition, new_material_card, normalize_material_card, selected_material_source
|
|
41
42
|
from hermes_ui.llm_providers import LLM_CARDS_KEY, LLM_PROVIDER_CATALOG, apply_llm_cards_to_settings, ensure_llm_provider_cards, new_llm_card, normalize_llm_card, provider_definition, test_llm_provider_card, stamp_test_result
|
|
42
|
-
from hermes_ui.media_providers import MEDIA_CARDS_KEY, MEDIA_IMAGE_ACTIVE_CARD_KEY, MEDIA_VIDEO_ACTIVE_CARD_KEY, apply_media_provider_cards_to_settings, ensure_media_provider_cards, media_provider_catalog, media_provider_definition, new_media_card, normalize_media_card
|
|
43
|
+
from hermes_ui.media_providers import MEDIA_CARDS_KEY, MEDIA_IMAGE_ACTIVE_CARD_KEY, MEDIA_VIDEO_ACTIVE_CARD_KEY, apply_media_provider_cards_to_settings, ensure_media_provider_cards, media_cards_for_pool, media_provider_catalog, media_provider_definition, new_media_card, normalize_media_card
|
|
43
44
|
from hermes_ui.music import list_music_files, materialize_suno_audio, request_suno_generation, store_music_file, store_voiceover_file
|
|
44
45
|
from hermes_ui.media_downloader import AUDIO_FORMATS, VIDEO_CONTAINERS, VIDEO_QUALITY_OPTIONS, MediaDownloadError, build_download_options, clear_media_download_history, dependency_status, download_media, list_media_downloads, media_download_file
|
|
45
46
|
from hermes_ui.notifications import clear_notifications, list_notifications, mark_all_notifications_read, mark_notification_read, notification_event_catalog, notification_preferences, record_notification, reconcile_persisted_notifications, save_notification_preferences, unread_notification_count
|
|
47
|
+
from hermes_ui.influencers import BACKEND_OPTIONS, DOCUMENT_EXTENSIONS, IMAGE_EXTENSIONS, backend_name, backend_status, get_repository, test_backend
|
|
46
48
|
from hermes_ui.logs import list_logs, logs_to_rows
|
|
47
49
|
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
|
|
48
|
-
from hermes_ui.api_key_tests import test_apify_credentials, 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
|
|
50
|
+
from hermes_ui.api_key_tests import test_apify_credentials, test_influencer_database, 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
|
|
49
51
|
from hermes_ui.tutorials import tutorial_body, tutorial_caption, tutorial_title
|
|
50
52
|
|
|
51
53
|
from hermes_ui.script_documents import list_script_documents, read_script_document, save_script_document, script_storage_path
|
|
@@ -63,6 +65,7 @@ from hermes_ui.thumbnails import (
|
|
|
63
65
|
)
|
|
64
66
|
from hermes_ui.draft_video import DRAFT_SETTING_SECTIONS, missing_content_fields, missing_setting_sections, normalise_saved_script, setting_widget_suffixes
|
|
65
67
|
from hermes_ui.creative_generation import CreativeGenerationError, generate_creative_package, generate_thumbnail_prompt, generate_title_and_keywords, generate_topic_for_channel, generate_video_keywords
|
|
68
|
+
from hermes_ui.media_generation import MediaGenerationError, generate_image_for_card, generate_video_for_card
|
|
66
69
|
from integrations.platforms import IntegrationResult, TikTokAdapter, YouTubeAdapter, fetch_channel_videos_public
|
|
67
70
|
from integrations.tiktok_public import fetch_public_tiktok_profile, normalize_tiktok_reference
|
|
68
71
|
from integrations.postiz import PostizAdapter
|
|
@@ -256,6 +259,9 @@ st.markdown("""
|
|
|
256
259
|
[data-testid="stMultiSelect"] [data-baseweb="tag"]:has(button[aria-label*="Instagram"]) { background:#e1306c !important; }
|
|
257
260
|
[data-testid="stMultiSelect"] [data-baseweb="tag"]:has(button[aria-label*="Facebook Pages"]) { background:#1877f2 !important; }
|
|
258
261
|
[data-testid="stMultiSelect"] [data-baseweb="tag"] svg { color:#ffffff !important; fill:#ffffff !important; }
|
|
262
|
+
/* Notificações globais: o histórico continua persistente, mas os avisos surgem sem abrir a aba. */
|
|
263
|
+
[data-testid="stToastContainer"] { top:auto !important; right:1rem !important; bottom:1rem !important; left:auto !important; z-index:100000 !important; }
|
|
264
|
+
[data-testid="stToast"] { max-width:min(28rem, calc(100vw - 2rem)); }
|
|
259
265
|
</style>
|
|
260
266
|
""", unsafe_allow_html=True)
|
|
261
267
|
|
|
@@ -338,6 +344,66 @@ def install_streamlit_content_translation() -> None:
|
|
|
338
344
|
install_streamlit_content_translation()
|
|
339
345
|
|
|
340
346
|
|
|
347
|
+
NOTIFICATION_TOAST_INTERVAL = "3s"
|
|
348
|
+
NOTIFICATION_TOAST_MAX_PER_CYCLE = 3
|
|
349
|
+
NOTIFICATION_TOAST_SEEN_KEY = "_thunderbolt_notification_toast_seen_ids"
|
|
350
|
+
NOTIFICATION_TOAST_INITIALISED_KEY = "_thunderbolt_notification_toast_initialised"
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _notification_toast_seen_ids() -> set[str]:
|
|
354
|
+
raw = st.session_state.get(NOTIFICATION_TOAST_SEEN_KEY, [])
|
|
355
|
+
if isinstance(raw, (list, tuple, set)):
|
|
356
|
+
return {str(item) for item in raw if str(item).strip()}
|
|
357
|
+
return set()
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _save_notification_toast_seen_ids(notification_ids: set[str]) -> None:
|
|
361
|
+
# Limitar o estado evita crescimento indefinido numa sessão longa.
|
|
362
|
+
st.session_state[NOTIFICATION_TOAST_SEEN_KEY] = sorted(notification_ids)[-500:]
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _render_notification_toast_cycle() -> None:
|
|
366
|
+
"""Reconcile completions and display only new unread notifications in this session."""
|
|
367
|
+
try:
|
|
368
|
+
reconcile_persisted_notifications()
|
|
369
|
+
except Exception:
|
|
370
|
+
# A camada visual nunca deve interromper o pipeline ou os workers.
|
|
371
|
+
return
|
|
372
|
+
entries = list_notifications(limit=500, unread_only=True)
|
|
373
|
+
current_ids = {str(item.get("id") or "") for item in entries if str(item.get("id") or "")}
|
|
374
|
+
seen_ids = _notification_toast_seen_ids()
|
|
375
|
+
if not st.session_state.get(NOTIFICATION_TOAST_INITIALISED_KEY, False):
|
|
376
|
+
# Não transformar o histórico antigo numa avalanche de pop-ups ao abrir a UI.
|
|
377
|
+
seen_ids.update(current_ids)
|
|
378
|
+
_save_notification_toast_seen_ids(seen_ids)
|
|
379
|
+
st.session_state[NOTIFICATION_TOAST_INITIALISED_KEY] = True
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
pending = [item for item in reversed(entries) if str(item.get("id") or "") not in seen_ids]
|
|
383
|
+
for item in pending[:NOTIFICATION_TOAST_MAX_PER_CYCLE]:
|
|
384
|
+
notification_id = str(item.get("id") or "")
|
|
385
|
+
if not notification_id:
|
|
386
|
+
continue
|
|
387
|
+
title = str(item.get("title") or item.get("label") or "Notificação").strip()
|
|
388
|
+
message = str(item.get("message") or "").strip()
|
|
389
|
+
body = f"**{title}**"
|
|
390
|
+
if message:
|
|
391
|
+
body = f"{body}\n\n{message}"
|
|
392
|
+
if hasattr(st, "toast"):
|
|
393
|
+
st.toast(body)
|
|
394
|
+
seen_ids.add(notification_id)
|
|
395
|
+
_save_notification_toast_seen_ids(seen_ids)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
if hasattr(st, "fragment"):
|
|
399
|
+
@st.fragment(run_every=NOTIFICATION_TOAST_INTERVAL)
|
|
400
|
+
def render_global_notification_toasts() -> None:
|
|
401
|
+
_render_notification_toast_cycle()
|
|
402
|
+
else:
|
|
403
|
+
def render_global_notification_toasts() -> None:
|
|
404
|
+
_render_notification_toast_cycle()
|
|
405
|
+
|
|
406
|
+
|
|
341
407
|
def localized_tab_labels(labels: list[str], language: str | None = None) -> list[str]:
|
|
342
408
|
"""Translate Streamlit tab labels while keeping widget keys and behaviour stable."""
|
|
343
409
|
selected_language = language_code(language or current_ui_language())
|
|
@@ -5320,7 +5386,7 @@ def render_settings():
|
|
|
5320
5386
|
key=f"settings_{key}",
|
|
5321
5387
|
)
|
|
5322
5388
|
|
|
5323
|
-
api_keys_tab, google_accounts_tab, material_sources_tab, voice_test_tab = render_localized_tabs(["API Keys", "Contas Google", "Fontes de Materiais", "Teste de Voz"])
|
|
5389
|
+
api_keys_tab, google_accounts_tab, material_sources_tab, ai_influencers_tab, voice_test_tab = render_localized_tabs(["API Keys", "Contas Google", "Fontes de Materiais", "AI Influencers", "Teste de Voz"])
|
|
5324
5390
|
|
|
5325
5391
|
with api_keys_tab:
|
|
5326
5392
|
with st.container(border=True):
|
|
@@ -5371,6 +5437,20 @@ def render_settings():
|
|
|
5371
5437
|
|
|
5372
5438
|
render_media_provider_cards(settings, embedded=True)
|
|
5373
5439
|
|
|
5440
|
+
with st.expander("Banco de Dados Influencers", expanded=False):
|
|
5441
|
+
st.caption("Escolha um backend para personagens e conteúdos. Supabase é o padrão remoto; SQLite mantém tudo localmente. O teste é read-only e não cria nem elimina registos.")
|
|
5442
|
+
influencer_db_backend = st.selectbox("Backend da base de dados de AI Influencers", list(BACKEND_OPTIONS), index=0 if str(settings.get("influencer_db_backend") or "Supabase") == "Supabase" else 1, key="settings_influencer_db_backend")
|
|
5443
|
+
db_cols = st.columns(2)
|
|
5444
|
+
with db_cols[0]:
|
|
5445
|
+
influencer_supabase_url = text_setting("Supabase Project URL", "influencer_supabase_url", help_text="URL do projecto, por exemplo https://project-id.supabase.co")
|
|
5446
|
+
influencer_supabase_key = text_setting("Supabase API key", "influencer_supabase_key", secret=True, help_text="Use uma chave com as permissões RLS adequadas. Nunca é colocada no GitHub ou nos logs.")
|
|
5447
|
+
influencer_supabase_bucket = text_setting("Supabase Storage bucket", "influencer_supabase_bucket", help_text="Bucket usado para imagens e documentos, por defeito ai-influencers.")
|
|
5448
|
+
with db_cols[1]:
|
|
5449
|
+
influencer_sqlite_path = text_setting("SQLite ficheiro local", "influencer_sqlite_path", help_text="Caminho absoluto ou relativo ao projecto. Por defeito: storage/state/ai_influencers.db")
|
|
5450
|
+
st.caption("SQLite usa a biblioteca standard do Python e cria o schema automaticamente ao testar ou usar o backend.")
|
|
5451
|
+
db_test_clicked = st.form_submit_button("Testar ligação do backend", use_container_width=True, key="api_test_influencer_database")
|
|
5452
|
+
st.caption("Para Supabase, aplique primeiro seed/references/ai_influencers_schema.sql no SQL Editor e configure o bucket/políticas RLS.")
|
|
5453
|
+
|
|
5374
5454
|
with st.expander("Voz, TTS e música — Azure Speech, restantes serviços e Suno", expanded=False):
|
|
5375
5455
|
st.caption("Cada serviço está separado no seu próprio cartão. Os botões de teste ficam dentro do cartão correspondente e fazem apenas diagnóstico, sem gerar áudio ou música.")
|
|
5376
5456
|
|
|
@@ -5518,6 +5598,15 @@ def render_settings():
|
|
|
5518
5598
|
widget_key="api_test_postiz",
|
|
5519
5599
|
)
|
|
5520
5600
|
|
|
5601
|
+
if db_test_clicked:
|
|
5602
|
+
test_settings = dict(settings)
|
|
5603
|
+
test_settings.update({"influencer_db_backend": influencer_db_backend, "influencer_supabase_url": influencer_supabase_url, "influencer_supabase_key": influencer_supabase_key, "influencer_supabase_bucket": influencer_supabase_bucket, "influencer_sqlite_path": influencer_sqlite_path})
|
|
5604
|
+
db_result = test_backend(test_settings)
|
|
5605
|
+
if db_result.get("ok"):
|
|
5606
|
+
st.success(db_result.get("message") or "Backend disponível.")
|
|
5607
|
+
else:
|
|
5608
|
+
st.error(db_result.get("message") or "O backend não está disponível.")
|
|
5609
|
+
|
|
5521
5610
|
save_all_settings = st.form_submit_button("Guardar configurações do Thunderbolt", type="primary")
|
|
5522
5611
|
if save_all_settings:
|
|
5523
5612
|
settings.update({
|
|
@@ -5525,6 +5614,7 @@ def render_settings():
|
|
|
5525
5614
|
"kaggle_username": kaggle_username.strip(), "kaggle_api_key": kaggle_api_key.strip(), "kaggle_kernel_slug": kaggle_kernel_slug.strip() or "thunderbolt-niche-finder",
|
|
5526
5615
|
"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),
|
|
5527
5616
|
"llm_rpm_limit_enabled": bool(llm_rpm_limit_enabled), "llm_rpm_limit": int(llm_rpm_limit), "llm_rpm_window_seconds": int(llm_rpm_window_seconds),
|
|
5617
|
+
"influencer_db_backend": influencer_db_backend, "influencer_supabase_url": influencer_supabase_url.strip(), "influencer_supabase_key": influencer_supabase_key.strip(), "influencer_supabase_bucket": influencer_supabase_bucket.strip() or "ai-influencers", "influencer_sqlite_path": influencer_sqlite_path.strip() or "storage/state/ai_influencers.db",
|
|
5528
5618
|
"azure_speech_key": azure_speech_key, "azure_speech_region": azure_speech_region,
|
|
5529
5619
|
"siliconflow_tts_api_key": siliconflow_tts_api_key, "minimax_tts_api_key": minimax_tts_api_key,
|
|
5530
5620
|
"minimax_tts_base_url": minimax_tts_base_url, "minimax_tts_model_id": minimax_tts_model_id, "minimax_tts_voice_id": minimax_tts_voice_id,
|
|
@@ -5554,6 +5644,9 @@ def render_settings():
|
|
|
5554
5644
|
with material_sources_tab:
|
|
5555
5645
|
render_material_source_api_keys(settings)
|
|
5556
5646
|
|
|
5647
|
+
with ai_influencers_tab:
|
|
5648
|
+
render_ai_influencers_api_status(settings)
|
|
5649
|
+
|
|
5557
5650
|
with voice_test_tab:
|
|
5558
5651
|
st.subheader("Teste de Voz")
|
|
5559
5652
|
st.caption("Este painel é exclusivamente um preview. O áudio gerado não altera vídeos, tarefas, Blueprints ou a configuração da pipeline.")
|
|
@@ -6277,9 +6370,9 @@ def main():
|
|
|
6277
6370
|
"Editor Python": render_python_editor,
|
|
6278
6371
|
"Download Mídia": render_media_download,
|
|
6279
6372
|
"AI Influencers": lambda: render_edit_placeholder("AI Influencers", "Seleccione uma das abas AI Influencers no menu expansível."),
|
|
6280
|
-
"Personagens": lambda:
|
|
6281
|
-
"Geração de Conteúdo IA": lambda:
|
|
6282
|
-
"Motion Control":
|
|
6373
|
+
"Personagens": lambda: render_ai_influencer_characters(read_json("settings.json", {})),
|
|
6374
|
+
"Geração de Conteúdo IA": lambda: render_ai_influencer_content(read_json("settings.json", {})),
|
|
6375
|
+
"Motion Control": render_ai_influencer_motion_control,
|
|
6283
6376
|
"UGC Products": lambda: render_edit_placeholder("UGC Products", ""),
|
|
6284
6377
|
"Redes Sociais": lambda: render_edit_placeholder("Redes Sociais", "Área reservada para a futura funcionalidade de redes sociais."),
|
|
6285
6378
|
"Analista Growth Youtube": lambda: render_edit_placeholder("Analista Growth Youtube", ""),
|
|
@@ -6295,10 +6388,7 @@ def main():
|
|
|
6295
6388
|
"Notificações": render_notifications,
|
|
6296
6389
|
"Logs": render_logs,
|
|
6297
6390
|
}
|
|
6298
|
-
|
|
6299
|
-
reconcile_persisted_notifications()
|
|
6300
|
-
except Exception:
|
|
6301
|
-
pass
|
|
6391
|
+
render_global_notification_toasts()
|
|
6302
6392
|
renderers.get(current_page, render_dashboard)()
|
|
6303
6393
|
|
|
6304
6394
|
if __name__ == "__main__":
|
|
@@ -272,6 +272,15 @@ def test_material_source_credentials(provider: str, api_key: str) -> dict[str, A
|
|
|
272
272
|
return _unsupported("Este provider de materiais não expõe um endpoint de diagnóstico seguro no cartão actual.")
|
|
273
273
|
|
|
274
274
|
|
|
275
|
+
def test_influencer_database(settings: Mapping[str, Any]) -> dict[str, Any]:
|
|
276
|
+
"""Verify the configured AI Influencers backend without generating or deleting data."""
|
|
277
|
+
try:
|
|
278
|
+
from hermes_ui.influencers import test_backend
|
|
279
|
+
return test_backend(settings)
|
|
280
|
+
except Exception:
|
|
281
|
+
return _result("error", "Não foi possível verificar o backend AI Influencers.")
|
|
282
|
+
|
|
283
|
+
|
|
275
284
|
def test_voice_provider(provider: str, settings: dict[str, Any]) -> dict[str, Any]:
|
|
276
285
|
"""Dispatch a non-generating check for one provider in the voice/music group."""
|
|
277
286
|
provider = str(provider or "").strip().lower()
|
|
@@ -297,6 +306,7 @@ __all__ = [
|
|
|
297
306
|
"test_azure_speech_credentials",
|
|
298
307
|
"test_elevenlabs_credentials",
|
|
299
308
|
"test_kaggle_credentials",
|
|
309
|
+
"test_influencer_database",
|
|
300
310
|
"test_material_source_credentials",
|
|
301
311
|
"test_minimax_credentials",
|
|
302
312
|
"test_nano_banana_credentials",
|