@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/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: render_edit_placeholder("Personagens", "Área reservada para a futura funcionalidade de personagens."),
6281
- "Geração de Conteúdo IA": lambda: render_edit_placeholder("Geração de Conteúdo IA", ""),
6282
- "Motion Control": lambda: render_edit_placeholder("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
- try:
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",