@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.
- package/MANUAL-INSTALACAO.md +8 -2
- package/README.md +8 -2
- package/app/main.py +89 -21
- package/hermes_ui/languages.py +197 -0
- package/hermes_ui/storage.py +2 -0
- package/integrations/moneyprinter_config.py +7 -0
- package/package.json +1 -1
package/MANUAL-INSTALACAO.md
CHANGED
|
@@ -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.
|
|
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;
|
|
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
|
-
##
|
|
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
|
|
130
|
-
#
|
|
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
|
-
|
|
383
|
-
index=
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
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", "00 – apenas 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
|
|
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
|
-
|
|
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
|
+
]
|
package/hermes_ui/storage.py
CHANGED
|
@@ -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