@danhachuel/thunderbolt 0.2.77 → 0.2.78

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Este manual descreve a instalação local da UI Thunderbolt, baseada no MoneyPrinterTurbo, utilizando o pacote npm `@danhachuel/thunderbolt`. O fluxo recomendado instala automaticamente o ambiente Python, as dependências da aplicação, as dependências do MoneyPrinterTurbo, o Streamlit e o suporte FFmpeg através de `imageio-ffmpeg`.
4
4
 
5
- > **Versão deste manual:** 0.2.75
5
+ > **Versão deste manual:** 0.2.78
6
6
  > **Pacote npm:** `@danhachuel/thunderbolt`
7
7
  > **Porta padrão da UI:** `localhost:3030`
8
8
  > **Repositório:** [github.com/DanHachuel/thunderbolt](https://github.com/DanHachuel/thunderbolt)
@@ -23,7 +23,7 @@ A instalação assistida cria um ambiente local separado para evitar misturar as
23
23
  | FFmpeg | Disponibilizado pelo pacote Python `imageio-ffmpeg` |
24
24
  | Porta da aplicação | `3030`, configurável com `THUNDERBOLT_PORT` |
25
25
 
26
- A aplicação não instala drivers de GPU, Docker, modelos Whisper ou credenciais externas. As chaves de API do MoneyPrinterTurbo são configuradas na aba **Configurações** e sincronizadas com o `config.toml` do clone local.
26
+ A aplicação não instala drivers de GPU, Docker, modelos Whisper ou credenciais externas. As chaves de API do MoneyPrinterTurbo são configuradas na aba **Configurações** e sincronizadas com o `config.toml` do clone local. O idioma da interface e o idioma dos vídeos são preferências independentes.
27
27
 
28
28
  O MoneyPrinterTurbo declara Python 3.11 ou superior como requisito e documenta a instalação com `uv` ou com `venv + pip`. A aplicação segue o mesmo princípio e adiciona um instalador assistido próprio.[1]
29
29
 
@@ -190,6 +190,12 @@ $env:THUNDERBOLT_VENV="C:\ContentHermes\.venv"
190
190
  npx --yes @danhachuel/thunderbolt install
191
191
  ```
192
192
 
193
+ ## Idiomas e selector de bandeira
194
+
195
+ O Thunderbolt inclui as dez opções de idioma usadas pelo fluxo MoneyPrinterTurbo: **🇺🇸 Inglês (en)**, **🇨🇳 Chinês Simplificado (zh)**, **🇩🇪 Alemão (de)**, **🇻🇳 Vietnamita (vi)**, **🇹🇷 Turco (tr)**, **🇧🇷 Português (pt)**, **🇷🇺 Russo (ru)**, **🇪🇸 Espanhol (es)**, **🇮🇩 Indonésio (id)** e **🇮🇹 Italiano (it)**. A criação de vídeos mantém ainda os rótulos históricos para não invalidar Blueprints e tarefas antigas.
196
+
197
+ No canto superior direito, imediatamente à esquerda de **Deploy**, o botão com a bandeira actual abre o selector do **Idioma da interface**. A escolha fica guardada como `ui_language` em `storage/state/settings.json` e, quando existe um caminho MoneyPrinterTurbo configurado, é sincronizada com `[ui].language` no `config.toml`. Na Criação de Vídeos e em Roteiros, o selector **Script Language** mostra bandeira, nome e código, mas guarda o código curto; essa escolha é sincronizada com `[ui].video_language`.
198
+
193
199
  ## 4. Diagnóstico antes de iniciar
194
200
 
195
201
  Depois da instalação, execute:
package/README.md CHANGED
@@ -15,7 +15,7 @@ A primeira versão implementa a camada UI independente com:
15
15
  | Blueprints Youtube | Leitura da pasta `storage/blueprints/`, upload/validação de JSON e criação a partir de link YouTube |
16
16
  | Brandings | Subaba própria dentro de Blueprints, upload/listagem de Brandings e criação conjunta com Blueprint |
17
17
  | Canais Youtube | Subabas de importação pública sem API Key, Canais em lote gmail por conta Google/YouTube via OAuth e cadastro manual independente; cartões com botão Editar, nicho/referências visível, Prompts do Canal/Blueprint, Narrador/voz e gestão dos últimos 10 vídeos |
18
- | Criação de Vídeos / Criação de Músicas | Subabas Criar vídeo e Vídeos; lotes; 51 rótulos de idioma; Pexels/Pixabay, full IA com Estilo IA e Apenas Música com agente musical; a segunda página reutiliza o mesmo fluxo com título próprio |
18
+ | Criação de Vídeos / Criação de Músicas | Subabas Criar vídeo e Vídeos; idiomas históricos preservados e dez códigos MoneyPrinterTurbo com bandeiras; Pexels/Pixabay, full IA com Estilo IA e Apenas Música com agente musical; a segunda página reutiliza o mesmo fluxo com título próprio |
19
19
  | Automação | Menu expansível com a subaba **Automação Youtube**, onde ficam os vídeos e canais, selectores editáveis de Blueprint/voz padrão, Automação ON, horário diário HH:MM e worker local baseado no relógio do computador |
20
20
  | AI Influencers | Menu expansível com Personagens, Redes Sociais e Tutorial Meta |
21
21
  | Niche Finder | Menu expansível com duas alternativas independentes: Niche Finder Kaggle e Niche Finder Apify, com parâmetros, execução e resultados separados |
@@ -46,7 +46,13 @@ Os dados são segredos de sessão. Os valores não aparecem em tabelas ou logs,
46
46
 
47
47
  Os adaptadores do MoneyPrinterTurbo e de publicação nas plataformas são ligados pelas configurações locais e pelos pontos de integração em `integrations/`. A UI não inventa dados quando um serviço externo ou credencial não está disponível.
48
48
 
49
- ## Navegação da UI 0.2.77
49
+ ## Idiomas e selector rápido da UI
50
+
51
+ O Thunderbolt mantém os idiomas históricos da criação de vídeos e acrescenta as dez opções canónicas do MoneyPrinterTurbo: **🇺🇸 Inglês (en)**, **🇨🇳 Chinês Simplificado (zh)**, **🇩🇪 Alemão (de)**, **🇻🇳 Vietnamita (vi)**, **🇹🇷 Turco (tr)**, **🇧🇷 Português (pt)**, **🇷🇺 Russo (ru)**, **🇪🇸 Espanhol (es)**, **🇮🇩 Indonésio (id)** e **🇮🇹 Italiano (it)**. Os códigos curtos são os valores persistidos e sincronizados com `ui.video_language` no `config.toml`; a bandeira e o nome são apenas a apresentação visual.
52
+
53
+ No canto superior direito da aplicação existe um botão compacto com a bandeira do idioma actual, colocado imediatamente à esquerda do botão **Deploy** do Streamlit. Ao abrir o botão, seleccione outro idioma; a preferência é guardada em `storage/state/settings.json` como `ui_language`, e a navegação lateral actualiza-se sem alterar o idioma seleccionado para os vídeos. A primeira implementação traduz o chrome principal da navegação e mantém os rótulos específicos das páginas compatíveis com o fluxo existente.
54
+
55
+ ## Navegação da UI 0.2.78
50
56
 
51
57
  A barra lateral mantém os níveis principais, nesta ordem: **Início**, **Niche Finder**, **Pipeline**, **Pipeline TikTok**, **Automação**, **Edição**, **AI Influencers** e **Configurações**. **Pipeline** é expansível e contém **Criação de Vídeos**, **Criação de Músicas**, **Roteiros** e **Upload**. **Automação** também é expansível e contém **Automação Youtube**. **Edição** é expansível e contém **Limpador de Metadados**, **Cortes**, **Editor Python** e **Download Mídia**, nessa ordem. **AI Influencers** é expansível e contém **Personagens**, **Redes Sociais** e **Tutorial Meta**, nessa ordem. **Niche Finder** é expansível e contém **Niche Finder Kaggle** e **Niche Finder Apify**. **Configurações** é expansível e contém **Canais Youtube**, **Blueprints Youtube**, **MCP**, **Contas Google**, **Configuração API** e **Notificações**. O Início reúne o dashboard e as filas do Pipeline, sem botões de acções rápidas.
52
58
 
package/app/main.py CHANGED
@@ -37,6 +37,7 @@ from hermes_ui.material_sources import material_api_keys, material_source_catalo
37
37
  from hermes_ui.music import list_music_files, materialize_suno_audio, request_suno_generation, store_music_file
38
38
  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
39
39
  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
40
+ from hermes_ui.languages import LANGUAGE_CODES, language_code, language_flag, language_label, ui_text, video_language_label, video_language_options
40
41
  from hermes_ui.script_documents import list_script_documents, read_script_document, save_script_document, script_storage_path
41
42
  from hermes_ui.script_generation import generate_script_document
42
43
  from hermes_ui.voice_preview import DEFAULT_SAMPLE, load_preview_file, synthesize_preview
@@ -126,8 +127,10 @@ VIDEO_LANGUAGE_OPTIONS = [
126
127
  "50 – Hausa",
127
128
  ]
128
129
 
129
- # Mantemos estes valores separados para que Criação de Vídeos e Roteiros partilhem
130
- # exactamente os mesmos selectores sem alterar a lista histórica de idiomas.
130
+ # Mantemos esta lista histórica para preservar Blueprints, testes e tarefas antigas.
131
+ # A UI acrescenta os dez códigos canónicos do MoneyPrinterTurbo com bandeiras.
132
+ VIDEO_LANGUAGE_SELECTION_OPTIONS = ["music", *LANGUAGE_CODES, *[item for item in VIDEO_LANGUAGE_OPTIONS if item not in {"01 – Inglês", "06 – Alemão", "15 – Italiano", "29 – Turco", "31 – Russo", "36 – Português (Brasil)", "39 – Mandarim", "41 – Espanhol (LatAm)", "42 – Vietnamita", "44 – Indonésio"}]]
133
+
131
134
  VIDEO_FORMAT_OPTIONS = ["wide", "shorts", "music"]
132
135
  VIDEO_CONCATENATION_OPTIONS = ["Random Concatenation (Recommended)", "Sequential Concatenation"]
133
136
  VIDEO_TRANSITION_OPTIONS = ["None", "Fade", "Dissolve"]
@@ -185,6 +188,12 @@ st.markdown("""
185
188
  [data-testid="stRadio"] label { border:1px solid #2a4052; border-radius:12px; padding:.65rem .8rem; background:#101b25; min-height:4.3rem; }
186
189
  [data-testid="stRadio"] label:has(input:checked) { border-color:#c59b55; background:linear-gradient(145deg, rgba(96,71,33,.42), rgba(17,27,37,.95)); }
187
190
  [data-testid="stStatusWidget"] { border-color:#2a4052 !important; background:#101b25 !important; }
191
+ /* Selector compacto de idioma, visualmente colocado à esquerda do Deploy do Streamlit. */
192
+ [data-testid="stPopover"] { position:fixed !important; top:0.34rem !important; right:4.75rem !important; z-index:100000 !important; }
193
+ [data-testid="stPopover"] > button { min-width:2.25rem !important; width:2.25rem !important; height:2.25rem !important; padding:0 !important; border:1px solid #2a4052 !important; border-radius:8px !important; background:#101b25 !important; }
194
+ [data-testid="stPopover"] > button p { font-size:1.12rem !important; line-height:1 !important; margin:0 !important; }
195
+ [data-testid="stPopover"] [data-testid="stPopoverBody"] { min-width:18rem; }
196
+ @media (max-width: 700px) { [data-testid="stPopover"] { right:3.2rem !important; } }
188
197
 
189
198
  /* Cores dos chips por identidade da plataforma, sem depender da ordem de selecção. */
190
199
  [data-testid="stMultiSelectTagsContainer"] span[data-tag] { color:#ffffff !important; border:0 !important; font-weight:700 !important; }
@@ -206,6 +215,58 @@ st.markdown("""
206
215
  """, unsafe_allow_html=True)
207
216
 
208
217
 
218
+ def current_ui_language() -> str:
219
+ settings = read_json("settings.json", {})
220
+ stored = st.session_state.get("ui_language") or settings.get("ui_language") or "pt"
221
+ normalized = language_code(stored)
222
+ st.session_state["ui_language"] = normalized
223
+ return normalized
224
+
225
+
226
+ def save_ui_language(value: str) -> str:
227
+ normalized = language_code(value)
228
+ settings = read_json("settings.json", {})
229
+ settings["ui_language"] = normalized
230
+ write_json("settings.json", settings)
231
+ try:
232
+ sync_moneyprinter_config(settings, str(settings.get("moneyprinter_path") or ""))
233
+ except Exception:
234
+ # The selector remains usable even when no MoneyPrinterTurbo path exists.
235
+ pass
236
+ st.session_state["ui_language"] = normalized
237
+ return normalized
238
+
239
+
240
+ def save_video_language(value: str) -> str:
241
+ normalized = "music" if str(value or "").strip().casefold() == "music" else language_code(value)
242
+ settings = read_json("settings.json", {})
243
+ settings["video_language"] = normalized
244
+ write_json("settings.json", settings)
245
+ try:
246
+ sync_moneyprinter_config(settings, str(settings.get("moneyprinter_path") or ""))
247
+ except Exception:
248
+ pass
249
+ st.session_state["video_language"] = normalized
250
+ return normalized
251
+
252
+
253
+ def render_ui_language_picker(language: str) -> None:
254
+ """Render the flag-only popover over the Streamlit toolbar area."""
255
+ current = language_code(language)
256
+ with st.popover(language_flag(current), help="Trocar idioma da interface"):
257
+ st.markdown(f"**{ui_text('Idioma da interface', current)}**")
258
+ selected = st.selectbox(
259
+ ui_text("Idioma da interface", current),
260
+ list(LANGUAGE_CODES),
261
+ index=list(LANGUAGE_CODES).index(current),
262
+ format_func=language_label,
263
+ key="ui_language_picker",
264
+ )
265
+ if selected != current:
266
+ save_ui_language(selected)
267
+ st.rerun()
268
+
269
+
209
270
  def card(label: str, value: str | int, note: str = ""):
210
271
  st.markdown(f'<div class="content-card"><div class="content-label">{label}</div><div class="content-value">{value}</div><div class="small-muted">{note}</div></div>', unsafe_allow_html=True)
211
272
 
@@ -377,10 +438,12 @@ def render_video_generation_settings(prefix: str, *, current_language: str = "")
377
438
  key=f"{prefix}_video_subject",
378
439
  placeholder="Ex.: How AI is changing everyday life",
379
440
  )
441
+ normalized_current_language = "music" if str(current_language or "").strip().casefold() in {"music", "00 – apenas música de fundo (sem falas)", "00 - apenas música de fundo (sem falas)"} else language_code(current_language)
380
442
  settings["script_language"] = st.selectbox(
381
443
  "Script Language",
382
- VIDEO_LANGUAGE_OPTIONS,
383
- index=VIDEO_LANGUAGE_OPTIONS.index(current_language) if current_language in VIDEO_LANGUAGE_OPTIONS else 0,
444
+ VIDEO_LANGUAGE_SELECTION_OPTIONS,
445
+ index=VIDEO_LANGUAGE_SELECTION_OPTIONS.index(normalized_current_language) if normalized_current_language in VIDEO_LANGUAGE_SELECTION_OPTIONS else 0,
446
+ format_func=video_language_label,
384
447
  key=f"{prefix}_script_language",
385
448
  )
386
449
  with subject_cols[1]:
@@ -603,7 +666,7 @@ def render_channel_edit_form(channel: dict, youtube_account_ids: list[str], yout
603
666
  edited_name = st.text_input("Nome do canal", value=str(channel.get("name") or ""))
604
667
  edited_url = st.text_input("URL", value=str(channel.get("url") or ""))
605
668
  edited_handle = st.text_input("Handle", value=str(channel.get("handle") or ""))
606
- edited_language = st.selectbox("Idioma", ["Português", "English", "Español", "Français", "Deutsch"], index=["Português", "English", "Español", "Français", "Deutsch"].index(channel.get("language")) if channel.get("language") in {"Português", "English", "Español", "Français", "Deutsch"} else 0)
669
+ edited_language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=list(LANGUAGE_CODES).index(language_code(channel.get("language") or "pt")), format_func=language_label)
607
670
  style_options = ["Pexels/Pixabay", "full_ia", "Apenas Música"]
608
671
  style_value = {"pexels": "Pexels/Pixabay", "music": "Apenas Música"}.get(str(channel.get("style_wide") or "pexels"), str(channel.get("style_wide") or "Pexels/Pixabay"))
609
672
  edited_style = st.selectbox("Estilo wide", style_options, index=style_options.index(style_value) if style_value in style_options else 0)
@@ -1052,7 +1115,7 @@ def render_channels():
1052
1115
  name = st.text_input("Nome do canal", value=imported.get("name", ""), key="yt_import_name")
1053
1116
  url = st.text_input("URL", value=imported.get("url", source if source.startswith("http") else ""), key="yt_import_url")
1054
1117
  handle = st.text_input("Handle", value=imported.get("handle", ""), key="yt_import_handle")
1055
- language = st.selectbox("Idioma", ["Português", "English", "Español", "Français", "Deutsch"], index=0, key="yt_import_language")
1118
+ language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=list(LANGUAGE_CODES).index(language_code(imported.get("language") or "pt")), format_func=language_label, key="yt_import_language")
1056
1119
  style = st.selectbox("Estilo wide", ["Pexels/Pixabay", "full_ia", "Apenas Música"], index=0, key="yt_import_style")
1057
1120
  blueprint = st.selectbox("Blueprint padrão do canal", 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")
1058
1121
  voice_options = voice_catalog(imported.get("default_voice") or imported.get("voice", ""))
@@ -1155,7 +1218,7 @@ def render_channels():
1155
1218
  with defaults_cols[1]:
1156
1219
  batch_voice = st.selectbox("Voz padrão", voice_options, format_func=lambda item: item or "Sem voz padrão", key="batch_channel_voice")
1157
1220
  with defaults_cols[2]:
1158
- batch_language = st.selectbox("Idioma", ["Português", "English", "Español", "Français", "Deutsch"], key="batch_channel_language")
1221
+ batch_language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=list(LANGUAGE_CODES).index("pt"), format_func=language_label, key="batch_channel_language")
1159
1222
  with defaults_cols[3]:
1160
1223
  batch_style = st.selectbox("Estilo wide", ["Pexels/Pixabay", "full_ia", "Apenas Música"], key="batch_channel_style")
1161
1224
  import_selected = st.form_submit_button("Cadastrar canais seleccionados", type="primary", use_container_width=True)
@@ -1201,7 +1264,7 @@ def render_channels():
1201
1264
  handle = st.text_input("Handle", placeholder="@seucanal", key="manual_channel_handle")
1202
1265
  description = st.text_area("Descrição", key="manual_channel_description")
1203
1266
  niche = st.text_input("Canais de Referência / Nicho", placeholder="Ex.: História militar, mistérios, ciência", key="manual_channel_niche")
1204
- language = st.selectbox("Idioma", ["Português", "English", "Español", "Français", "Deutsch"], index=0, key="manual_channel_language")
1267
+ language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=list(LANGUAGE_CODES).index("pt"), format_func=language_label, key="manual_channel_language")
1205
1268
  style = st.selectbox("Estilo wide", ["Pexels/Pixabay", "full_ia", "Apenas Música"], index=0, key="manual_channel_style")
1206
1269
  manual_blueprint_items = blueprint_catalog()
1207
1270
  manual_blueprint_ids = [item[0] for item in manual_blueprint_items]
@@ -1361,10 +1424,11 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
1361
1424
  )
1362
1425
  mode = {"Canal específico": "single", "Lote no mesmo canal": "same_channel", "Lote geral": "general"}[mode_label]
1363
1426
  selected_one: dict[str, Any] | None = None
1364
- legacy_language = st.session_state.get("video_language")
1365
- legacy_language_map = {"Português": "36Português (Brasil)", "English": "01 Inglês", "Español": "41 Espanhol (LatAm)"}
1366
- if legacy_language not in VIDEO_LANGUAGE_OPTIONS:
1367
- st.session_state["video_language"] = legacy_language_map.get(legacy_language, VIDEO_LANGUAGE_OPTIONS[0])
1427
+ legacy_language = st.session_state.get("video_language") or read_json("settings.json", {}).get("video_language")
1428
+ if str(legacy_language or "").strip().casefold() in {"music", "00apenas música de fundo (sem falas)", "00 - apenas música de fundo (sem falas)"}:
1429
+ st.session_state["video_language"] = "music"
1430
+ else:
1431
+ st.session_state["video_language"] = language_code(legacy_language or "pt")
1368
1432
  generation_settings: dict[str, Any] = {}
1369
1433
  if mode == "general":
1370
1434
  selected = [str(channel["id"]) for channel in all_channels if channel.get("id")]
@@ -1625,6 +1689,7 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
1625
1689
  fmt = generation_settings["video_format"]
1626
1690
  submitted = st.form_submit_button("Criar tarefas", type="primary")
1627
1691
  if submitted:
1692
+ save_video_language(language)
1628
1693
  style = {"Pexels/Pixabay": "pexels", "full_ia": "full_ia", "Apenas Música": "music"}[wide_style_label]
1629
1694
  if style == "music" and not music_path:
1630
1695
  st.error("Escolha, carregue ou gere uma música antes de criar o vídeo Apenas Música.")
@@ -1753,7 +1818,7 @@ def render_scripts():
1753
1818
  legacy_script_language = str(st.session_state.get("script_language") or "")
1754
1819
  script_settings = render_video_generation_settings(
1755
1820
  "pipeline_scripts",
1756
- current_language=legacy_script_language if legacy_script_language in VIDEO_LANGUAGE_OPTIONS else "",
1821
+ current_language=legacy_script_language or str(read_json("settings.json", {}).get("video_language") or "pt"),
1757
1822
  )
1758
1823
  language = script_settings["script_language"]
1759
1824
  structure_notes = script_settings["script_structure_notes"]
@@ -4031,13 +4096,16 @@ def main():
4031
4096
  if current_page not in {item[0] for item in top_pages + pipeline_items + pipeline_tiktok_items + automation_items + edition_items + models_ai_items + niche_finder_items + settings_items}:
4032
4097
  current_page = "Início"
4033
4098
  st.session_state["page"] = current_page
4099
+ ui_language = current_ui_language()
4100
+ render_ui_language_picker(ui_language)
4034
4101
 
4035
4102
  def navigate(target: str):
4036
4103
  st.session_state["page"] = target
4037
4104
  st.rerun()
4038
4105
 
4039
4106
  def render_nav_button(target: str, icon: str, label: str, *, child: bool = False):
4040
- if st.button(label, key=f"nav_{target}", icon=icon, use_container_width=True, type="primary" if current_page == target else "secondary"):
4107
+ display_label = ui_text(label, ui_language)
4108
+ if st.button(display_label, key=f"nav_{target}", icon=icon, use_container_width=True, type="primary" if current_page == target else "secondary"):
4041
4109
  navigate(target)
4042
4110
 
4043
4111
  with st.sidebar:
@@ -4045,31 +4113,31 @@ def main():
4045
4113
  st.markdown(f'<div class="tb-brand"><span class="tb-brand-name">Thunderbolt</span>{version_markup}</div>', unsafe_allow_html=True)
4046
4114
  for target, icon, label in top_pages:
4047
4115
  if target == "Pipeline":
4048
- with st.expander("Pipeline", expanded=current_page in {item[0] for item in pipeline_items}, icon=":material/account_tree:"):
4116
+ with st.expander(ui_text("Pipeline", ui_language), expanded=current_page in {item[0] for item in pipeline_items}, icon=":material/account_tree:"):
4049
4117
  for child_target, child_icon, child_label in pipeline_items:
4050
4118
  render_nav_button(child_target, child_icon, child_label, child=True)
4051
4119
  elif target == "Pipeline TikTok":
4052
- with st.expander("Pipeline TikTok", expanded=current_page in {item[0] for item in pipeline_tiktok_items}, icon=":material/video_library:"):
4120
+ with st.expander(ui_text("Pipeline TikTok", ui_language), expanded=current_page in {item[0] for item in pipeline_tiktok_items}, icon=":material/video_library:"):
4053
4121
  for child_target, child_icon, child_label in pipeline_tiktok_items:
4054
4122
  render_nav_button(child_target, child_icon, child_label, child=True)
4055
4123
  elif target == "Automação":
4056
- with st.expander("Automação", expanded=current_page in {item[0] for item in automation_items}, icon=":material/schedule:"):
4124
+ with st.expander(ui_text("Automação", ui_language), expanded=current_page in {item[0] for item in automation_items}, icon=":material/schedule:"):
4057
4125
  for child_target, child_icon, child_label in automation_items:
4058
4126
  render_nav_button(child_target, child_icon, child_label, child=True)
4059
4127
  elif target == "Edição":
4060
- with st.expander("Edição", expanded=current_page in {item[0] for item in edition_items}, icon=":material/edit:"):
4128
+ with st.expander(ui_text("Edição", ui_language), expanded=current_page in {item[0] for item in edition_items}, icon=":material/edit:"):
4061
4129
  for child_target, child_icon, child_label in edition_items:
4062
4130
  render_nav_button(child_target, child_icon, child_label, child=True)
4063
4131
  elif target == "AI Influencers":
4064
- with st.expander("AI Influencers", expanded=current_page in {item[0] for item in models_ai_items}, icon=":material/smart_toy:"):
4132
+ with st.expander(ui_text("AI Influencers", ui_language), expanded=current_page in {item[0] for item in models_ai_items}, icon=":material/smart_toy:"):
4065
4133
  for child_target, child_icon, child_label in models_ai_items:
4066
4134
  render_nav_button(child_target, child_icon, child_label, child=True)
4067
4135
  elif target == "Niche Finder":
4068
- with st.expander("Niche Finder", expanded=current_page in {item[0] for item in niche_finder_items}, icon=":material/search:"):
4136
+ with st.expander(ui_text("Niche Finder", ui_language), expanded=current_page in {item[0] for item in niche_finder_items}, icon=":material/search:"):
4069
4137
  for child_target, child_icon, child_label in niche_finder_items:
4070
4138
  render_nav_button(child_target, child_icon, child_label, child=True)
4071
4139
  elif target == "Configurações":
4072
- with st.expander("Configurações", expanded=current_page in {item[0] for item in settings_items}, icon=":material/settings:"):
4140
+ with st.expander(ui_text("Configurações", ui_language), expanded=current_page in {item[0] for item in settings_items}, icon=":material/settings:"):
4073
4141
  for child_target, child_icon, child_label in settings_items:
4074
4142
  render_nav_button(child_target, child_icon, child_label, child=True)
4075
4143
  else:
@@ -0,0 +1,197 @@
1
+ """Canonical UI and video language catalog shared by Thunderbolt screens."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ # The persisted value is always the short MoneyPrinterTurbo-compatible code.
9
+ LANGUAGE_CATALOG: tuple[dict[str, str], ...] = (
10
+ {"code": "en", "name": "Inglês", "flag": "🇺🇸", "locale": "en-US"},
11
+ {"code": "zh", "name": "Chinês Simplificado", "flag": "🇨🇳", "locale": "zh-CN"},
12
+ {"code": "de", "name": "Alemão", "flag": "🇩🇪", "locale": "de-DE"},
13
+ {"code": "vi", "name": "Vietnamita", "flag": "🇻🇳", "locale": "vi-VN"},
14
+ {"code": "tr", "name": "Turco", "flag": "🇹🇷", "locale": "tr-TR"},
15
+ {"code": "pt", "name": "Português", "flag": "🇧🇷", "locale": "pt-BR"},
16
+ {"code": "ru", "name": "Russo", "flag": "🇷🇺", "locale": "ru-RU"},
17
+ {"code": "es", "name": "Espanhol", "flag": "🇪🇸", "locale": "es-ES"},
18
+ {"code": "id", "name": "Indonésio", "flag": "🇮🇩", "locale": "id-ID"},
19
+ {"code": "it", "name": "Italiano", "flag": "🇮🇹", "locale": "it-IT"},
20
+ )
21
+
22
+ LANGUAGE_BY_CODE = {item["code"]: item for item in LANGUAGE_CATALOG}
23
+ LANGUAGE_CODES = tuple(item["code"] for item in LANGUAGE_CATALOG)
24
+
25
+ # Legacy labels used by existing channels, scripts and tests. They remain
26
+ # readable while new UI selections persist only the canonical short code.
27
+ LEGACY_LANGUAGE_CODES = {
28
+ "01 – inglês": "en",
29
+ "01 - inglês": "en",
30
+ "06 – alemão": "de",
31
+ "06 - alemão": "de",
32
+ "15 – italiano": "it",
33
+ "15 - italiano": "it",
34
+ "29 – turco": "tr",
35
+ "29 - turco": "tr",
36
+ "31 – russo": "ru",
37
+ "31 - russo": "ru",
38
+ "36 – português (brasil)": "pt",
39
+ "36 - português (brasil)": "pt",
40
+ "39 – mandarim": "zh",
41
+ "39 - mandarim": "zh",
42
+ "13 – espanhol (espanha)": "es",
43
+ "41 - espanhol (latam)": "es",
44
+ "42 – vietnamita": "vi",
45
+ "42 - vietnamita": "vi",
46
+ "44 – indonésio": "id",
47
+ "44 - indonésio": "id",
48
+ "13 – espanhol (espanha)": "es",
49
+ "13 - espanhol (espanha)": "es",
50
+ "37 – cantonês": "zh",
51
+ "37 - cantonês": "zh",
52
+ "english": "en",
53
+ "inglês": "en",
54
+ "inglés": "en",
55
+ "zh": "zh",
56
+ "chinês": "zh",
57
+ "chinês simplificado": "zh",
58
+ "deutsch": "de",
59
+ "alemão": "de",
60
+ "vietnamita": "vi",
61
+ "turco": "tr",
62
+ "português": "pt",
63
+ "português (brasil)": "pt",
64
+ "portuguese": "pt",
65
+ "ru": "ru",
66
+ "russo": "ru",
67
+ "español": "es",
68
+ "espanhol": "es",
69
+ "indonésio": "id",
70
+ "bahasa indonesia": "id",
71
+ "italiano": "it",
72
+ }
73
+
74
+
75
+ def language_code(value: Any, default: str = "pt") -> str:
76
+ """Normalize a canonical code or a legacy display label to a code."""
77
+ raw = str(value or "").strip()
78
+ if raw in LANGUAGE_BY_CODE:
79
+ return raw
80
+ lowered = raw.casefold()
81
+ if lowered in LEGACY_LANGUAGE_CODES:
82
+ return LEGACY_LANGUAGE_CODES[lowered]
83
+ for item in LANGUAGE_CATALOG:
84
+ if lowered in {item["name"].casefold(), item["locale"].casefold()}:
85
+ return item["code"]
86
+ return default if default in LANGUAGE_BY_CODE else "pt"
87
+
88
+
89
+ def language_label(value: Any, *, include_code: bool = True) -> str:
90
+ """Return the visual label with the requested flag and optional code."""
91
+ code = language_code(value)
92
+ item = LANGUAGE_BY_CODE[code]
93
+ suffix = f" ({item['code']})" if include_code else ""
94
+ return f"{item['flag']} {item['name']}{suffix}"
95
+
96
+
97
+ def language_option_labels(*, include_code: bool = True) -> list[str]:
98
+ return [language_label(item["code"], include_code=include_code) for item in LANGUAGE_CATALOG]
99
+
100
+
101
+ def language_option_codes() -> list[str]:
102
+ return list(LANGUAGE_CODES)
103
+
104
+
105
+ UI_TRANSLATIONS: dict[str, dict[str, str]] = {
106
+ "en": {
107
+ "Início": "Home", "Niche Finder": "Niche Finder", "Pipeline": "Pipeline", "Pipeline TikTok": "TikTok Pipeline",
108
+ "Automação": "Automation", "Edição": "Editing", "AI Influencers": "AI Influencers", "Configurações": "Settings",
109
+ "Criação de Vídeos": "Video Creation", "Criação de Músicas": "Music Creation", "Roteiros": "Scripts", "Upload": "Upload",
110
+ "Prompts Master": "Prompt Masters", "Contas TikTok": "TikTok Accounts", "Automação Youtube": "YouTube Automation",
111
+ "Canais Youtube": "YouTube Channels", "Blueprints Youtube": "YouTube Blueprints", "Contas Google": "Google Accounts",
112
+ "Configuração API": "API Configuration", "Notificações": "Notifications", "Niche Finder Kaggle": "Niche Finder Kaggle",
113
+ "Niche Finder Apify": "Niche Finder Apify", "Limpador de Metadados": "Metadata Cleaner", "Cortes": "Cuts",
114
+ "Editor Python": "Python Editor", "Download Mídia": "Media Download", "Personagens": "Characters", "Redes Sociais": "Social Networks",
115
+ "Tutorial Meta": "Meta Tutorial", "Idioma da interface": "Interface language",
116
+ },
117
+ "zh": {
118
+ "Início": "首页", "Pipeline": "视频流程", "Pipeline TikTok": "TikTok 流程", "Automação": "自动化", "Edição": "编辑", "Configurações": "设置",
119
+ "Criação de Vídeos": "视频创作", "Criação de Músicas": "音乐创作", "Roteiros": "脚本", "Upload": "上传", "Contas Google": "Google 账户", "Notificações": "通知", "Idioma da interface": "界面语言",
120
+ },
121
+ "de": {
122
+ "Início": "Startseite", "Pipeline": "Pipeline", "Pipeline TikTok": "TikTok-Pipeline", "Automação": "Automatisierung", "Edição": "Bearbeitung", "Configurações": "Einstellungen",
123
+ "Criação de Vídeos": "Videoerstellung", "Criação de Músicas": "Musikerstellung", "Roteiros": "Skripte", "Upload": "Upload", "Contas Google": "Google-Konten", "Notificações": "Benachrichtigungen", "Idioma da interface": "Oberflächensprache",
124
+ },
125
+ "vi": {
126
+ "Início": "Trang chủ", "Pipeline": "Quy trình", "Pipeline TikTok": "Quy trình TikTok", "Automação": "Tự động hóa", "Edição": "Chỉnh sửa", "Configurações": "Cài đặt",
127
+ "Criação de Vídeos": "Tạo video", "Criação de Músicas": "Tạo nhạc", "Roteiros": "Kịch bản", "Upload": "Tải lên", "Contas Google": "Tài khoản Google", "Notificações": "Thông báo", "Idioma da interface": "Ngôn ngữ giao diện",
128
+ },
129
+ "tr": {
130
+ "Início": "Ana sayfa", "Pipeline": "Akış", "Pipeline TikTok": "TikTok akışı", "Automação": "Otomasyon", "Edição": "Düzenleme", "Configurações": "Ayarlar",
131
+ "Criação de Vídeos": "Video oluşturma", "Criação de Músicas": "Müzik oluşturma", "Roteiros": "Senaryolar", "Upload": "Yükleme", "Contas Google": "Google hesapları", "Notificações": "Bildirimler", "Idioma da interface": "Arayüz dili",
132
+ },
133
+ "ru": {
134
+ "Início": "Главная", "Pipeline": "Конвейер", "Pipeline TikTok": "Конвейер TikTok", "Automação": "Автоматизация", "Edição": "Редактирование", "Configurações": "Настройки",
135
+ "Criação de Vídeos": "Создание видео", "Criação de Músicas": "Создание музыки", "Roteiros": "Сценарии", "Upload": "Загрузка", "Contas Google": "Аккаунты Google", "Notificações": "Уведомления", "Idioma da interface": "Язык интерфейса",
136
+ },
137
+ "es": {
138
+ "Início": "Inicio", "Pipeline": "Flujo", "Pipeline TikTok": "Flujo de TikTok", "Automação": "Automatización", "Edição": "Edición", "Configurações": "Configuración",
139
+ "Criação de Vídeos": "Creación de vídeos", "Criação de Músicas": "Creación de música", "Roteiros": "Guiones", "Upload": "Carga", "Contas Google": "Cuentas de Google", "Notificações": "Notificaciones", "Idioma da interface": "Idioma de la interfaz",
140
+ },
141
+ "id": {
142
+ "Início": "Beranda", "Pipeline": "Alur", "Pipeline TikTok": "Alur TikTok", "Automação": "Otomatisasi", "Edição": "Penyuntingan", "Configurações": "Pengaturan",
143
+ "Criação de Vídeos": "Pembuatan video", "Criação de Músicas": "Pembuatan musik", "Roteiros": "Skrip", "Upload": "Unggah", "Contas Google": "Akun Google", "Notificações": "Notifikasi", "Idioma da interface": "Bahasa antarmuka",
144
+ },
145
+ "it": {
146
+ "Início": "Home", "Pipeline": "Pipeline", "Pipeline TikTok": "Pipeline TikTok", "Automação": "Automazione", "Edição": "Modifica", "Configurações": "Impostazioni",
147
+ "Criação de Vídeos": "Creazione video", "Criação de Músicas": "Creazione musicale", "Roteiros": "Script", "Upload": "Caricamento", "Contas Google": "Account Google", "Notificações": "Notifiche", "Idioma da interface": "Lingua dell'interfaccia",
148
+ },
149
+ "pt": {},
150
+ }
151
+
152
+
153
+ def ui_text(value: str, language: Any = "pt") -> str:
154
+ code = language_code(language)
155
+ return UI_TRANSLATIONS.get(code, {}).get(value, value)
156
+
157
+
158
+ def language_flag(value: Any) -> str:
159
+ return LANGUAGE_BY_CODE[language_code(value)]["flag"]
160
+
161
+
162
+ def language_locale(value: Any) -> str:
163
+ return LANGUAGE_BY_CODE[language_code(value)]["locale"]
164
+
165
+
166
+ def video_language_label(value: Any) -> str:
167
+ """Format a video-language value while preserving all historical options."""
168
+ raw = str(value or "").strip()
169
+ if raw.casefold() in {
170
+ "music",
171
+ "00 – apenas música de fundo (sem falas)",
172
+ "00 - apenas música de fundo (sem falas)",
173
+ }:
174
+ return "🎵 Apenas Música de Fundo (Sem Falas)"
175
+ normalized = language_code(raw, default="")
176
+ if normalized in LANGUAGE_BY_CODE and (raw.casefold() in LEGACY_LANGUAGE_CODES or raw in LANGUAGE_BY_CODE):
177
+ return language_label(normalized)
178
+ return raw
179
+
180
+
181
+ def video_language_options() -> list[str]:
182
+ return ["music", *LANGUAGE_CODES]
183
+
184
+
185
+ __all__ = [
186
+ "LANGUAGE_CATALOG",
187
+ "LANGUAGE_BY_CODE",
188
+ "LANGUAGE_CODES",
189
+ "language_code",
190
+ "language_flag",
191
+ "language_label",
192
+ "language_locale",
193
+ "language_option_codes",
194
+ "language_option_labels",
195
+ "video_language_label",
196
+ "video_language_options",
197
+ ]
@@ -193,6 +193,8 @@ DEFAULTS: dict[str, Any] = {
193
193
  "log_level": "DEBUG",
194
194
  "listen_host": "127.0.0.1",
195
195
  "listen_port": 8080,
196
+ "ui_language": "pt",
197
+ "video_language": "pt",
196
198
  "video_source": "pexels",
197
199
  "match_materials_to_script": False,
198
200
  "endpoint": "",
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from pathlib import Path
4
4
  from typing import Any
5
5
 
6
+ from hermes_ui.languages import language_code
6
7
  from hermes_ui.material_sources import all_material_api_keys, selected_material_source
7
8
 
8
9
  try:
@@ -32,6 +33,7 @@ def build_moneyprinter_config(settings: dict[str, Any], existing: dict[str, Any]
32
33
  elevenlabs = dict(config.get("elevenlabs") or {})
33
34
  chatterbox = dict(config.get("chatterbox") or {})
34
35
  proxy = dict(config.get("proxy") or {})
36
+ ui = dict(config.get("ui") or {})
35
37
 
36
38
  app_map = {
37
39
  "llm_provider": "llm_provider",
@@ -123,6 +125,11 @@ def build_moneyprinter_config(settings: dict[str, Any], existing: dict[str, Any]
123
125
  app["video_source"] = selected_material_source(settings)
124
126
  config["app"] = app
125
127
 
128
+ ui["language"] = language_code(settings.get("ui_language", ui.get("language", "pt")))
129
+ requested_video_language = str(settings.get("video_language") or ui.get("video_language") or "").strip()
130
+ ui["video_language"] = "" if requested_video_language.casefold() == "music" else language_code(requested_video_language)
131
+ config["ui"] = ui
132
+
126
133
  whisper["model_size"] = settings.get("whisper_model_size", whisper.get("model_size", "large-v3"))
127
134
  whisper["device"] = settings.get("whisper_device", whisper.get("device", "cpu"))
128
135
  whisper["compute_type"] = settings.get("whisper_compute_type", whisper.get("compute_type", "int8"))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.2.77",
3
+ "version": "0.2.78",
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",