@danhachuel/thunderbolt 0.2.77 → 0.2.79
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 +97 -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.79
|
|
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 **à direita de Deploy e antes do menu principal**, o botão com a bandeira actual abre o selector do **Idioma da interface**. O espaço à esquerda de **Deploy** fica livre para o indicador de execução do Streamlit. 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 **à direita do botão Deploy e antes do menu principal** do Streamlit. O espaço à esquerda de **Deploy** fica livre para o indicador de execução. 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.79
|
|
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,20 @@ 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 à direita do Deploy e antes do menu. */
|
|
192
|
+
[data-testid="stPopover"] { position:fixed !important; top:0.20rem !important; right:2.5rem !important; width:2.55rem !important; min-width:2.55rem !important; max-width:2.55rem !important; z-index:100000 !important; pointer-events:auto !important; }
|
|
193
|
+
[data-testid="stAppDeployButton"] { transform:translateX(-3rem) !important; }
|
|
194
|
+
[data-testid="stMainMenu"] { transform:translateX(1rem) !important; }
|
|
195
|
+
[data-testid="stPopover"] > div,
|
|
196
|
+
[data-testid="stPopover"] [data-testid="stPopoverButton"] { width:2.55rem !important; min-width:2.55rem !important; max-width:2.55rem !important; }
|
|
197
|
+
[data-testid="stPopover"] [data-testid="stPopoverButton"] { height:2.25rem !important; padding:0 !important; border:1px solid #2a4052 !important; border-radius:8px !important; background:#101b25 !important; }
|
|
198
|
+
[data-testid="stPopover"] [data-testid="stPopoverButton"] p { font-size:1.12rem !important; line-height:1 !important; margin:0 !important; }
|
|
199
|
+
[data-testid="stPopover"] [data-testid="stPopoverBody"] { min-width:18rem; }
|
|
200
|
+
@media (max-width: 700px) {
|
|
201
|
+
[data-testid="stPopover"] { right:2.5rem !important; }
|
|
202
|
+
[data-testid="stAppDeployButton"] { transform:translateX(-3rem) !important; }
|
|
203
|
+
[data-testid="stMainMenu"] { transform:translateX(1rem) !important; }
|
|
204
|
+
}
|
|
188
205
|
|
|
189
206
|
/* Cores dos chips por identidade da plataforma, sem depender da ordem de selecção. */
|
|
190
207
|
[data-testid="stMultiSelectTagsContainer"] span[data-tag] { color:#ffffff !important; border:0 !important; font-weight:700 !important; }
|
|
@@ -206,6 +223,58 @@ st.markdown("""
|
|
|
206
223
|
""", unsafe_allow_html=True)
|
|
207
224
|
|
|
208
225
|
|
|
226
|
+
def current_ui_language() -> str:
|
|
227
|
+
settings = read_json("settings.json", {})
|
|
228
|
+
stored = st.session_state.get("ui_language") or settings.get("ui_language") or "pt"
|
|
229
|
+
normalized = language_code(stored)
|
|
230
|
+
st.session_state["ui_language"] = normalized
|
|
231
|
+
return normalized
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def save_ui_language(value: str) -> str:
|
|
235
|
+
normalized = language_code(value)
|
|
236
|
+
settings = read_json("settings.json", {})
|
|
237
|
+
settings["ui_language"] = normalized
|
|
238
|
+
write_json("settings.json", settings)
|
|
239
|
+
try:
|
|
240
|
+
sync_moneyprinter_config(settings, str(settings.get("moneyprinter_path") or ""))
|
|
241
|
+
except Exception:
|
|
242
|
+
# The selector remains usable even when no MoneyPrinterTurbo path exists.
|
|
243
|
+
pass
|
|
244
|
+
st.session_state["ui_language"] = normalized
|
|
245
|
+
return normalized
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def save_video_language(value: str) -> str:
|
|
249
|
+
normalized = "music" if str(value or "").strip().casefold() == "music" else language_code(value)
|
|
250
|
+
settings = read_json("settings.json", {})
|
|
251
|
+
settings["video_language"] = normalized
|
|
252
|
+
write_json("settings.json", settings)
|
|
253
|
+
try:
|
|
254
|
+
sync_moneyprinter_config(settings, str(settings.get("moneyprinter_path") or ""))
|
|
255
|
+
except Exception:
|
|
256
|
+
pass
|
|
257
|
+
st.session_state["video_language"] = normalized
|
|
258
|
+
return normalized
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def render_ui_language_picker(language: str) -> None:
|
|
262
|
+
"""Render the flag-only popover over the Streamlit toolbar area."""
|
|
263
|
+
current = language_code(language)
|
|
264
|
+
with st.popover(language_flag(current), help="Trocar idioma da interface"):
|
|
265
|
+
st.markdown(f"**{ui_text('Idioma da interface', current)}**")
|
|
266
|
+
selected = st.selectbox(
|
|
267
|
+
ui_text("Idioma da interface", current),
|
|
268
|
+
list(LANGUAGE_CODES),
|
|
269
|
+
index=list(LANGUAGE_CODES).index(current),
|
|
270
|
+
format_func=language_label,
|
|
271
|
+
key="ui_language_picker",
|
|
272
|
+
)
|
|
273
|
+
if selected != current:
|
|
274
|
+
save_ui_language(selected)
|
|
275
|
+
st.rerun()
|
|
276
|
+
|
|
277
|
+
|
|
209
278
|
def card(label: str, value: str | int, note: str = ""):
|
|
210
279
|
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
280
|
|
|
@@ -377,10 +446,12 @@ def render_video_generation_settings(prefix: str, *, current_language: str = "")
|
|
|
377
446
|
key=f"{prefix}_video_subject",
|
|
378
447
|
placeholder="Ex.: How AI is changing everyday life",
|
|
379
448
|
)
|
|
449
|
+
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
450
|
settings["script_language"] = st.selectbox(
|
|
381
451
|
"Script Language",
|
|
382
|
-
|
|
383
|
-
index=
|
|
452
|
+
VIDEO_LANGUAGE_SELECTION_OPTIONS,
|
|
453
|
+
index=VIDEO_LANGUAGE_SELECTION_OPTIONS.index(normalized_current_language) if normalized_current_language in VIDEO_LANGUAGE_SELECTION_OPTIONS else 0,
|
|
454
|
+
format_func=video_language_label,
|
|
384
455
|
key=f"{prefix}_script_language",
|
|
385
456
|
)
|
|
386
457
|
with subject_cols[1]:
|
|
@@ -603,7 +674,7 @@ def render_channel_edit_form(channel: dict, youtube_account_ids: list[str], yout
|
|
|
603
674
|
edited_name = st.text_input("Nome do canal", value=str(channel.get("name") or ""))
|
|
604
675
|
edited_url = st.text_input("URL", value=str(channel.get("url") or ""))
|
|
605
676
|
edited_handle = st.text_input("Handle", value=str(channel.get("handle") or ""))
|
|
606
|
-
edited_language = st.selectbox("Idioma",
|
|
677
|
+
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
678
|
style_options = ["Pexels/Pixabay", "full_ia", "Apenas Música"]
|
|
608
679
|
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
680
|
edited_style = st.selectbox("Estilo wide", style_options, index=style_options.index(style_value) if style_value in style_options else 0)
|
|
@@ -1052,7 +1123,7 @@ def render_channels():
|
|
|
1052
1123
|
name = st.text_input("Nome do canal", value=imported.get("name", ""), key="yt_import_name")
|
|
1053
1124
|
url = st.text_input("URL", value=imported.get("url", source if source.startswith("http") else ""), key="yt_import_url")
|
|
1054
1125
|
handle = st.text_input("Handle", value=imported.get("handle", ""), key="yt_import_handle")
|
|
1055
|
-
language = st.selectbox("Idioma",
|
|
1126
|
+
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
1127
|
style = st.selectbox("Estilo wide", ["Pexels/Pixabay", "full_ia", "Apenas Música"], index=0, key="yt_import_style")
|
|
1057
1128
|
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
1129
|
voice_options = voice_catalog(imported.get("default_voice") or imported.get("voice", ""))
|
|
@@ -1155,7 +1226,7 @@ def render_channels():
|
|
|
1155
1226
|
with defaults_cols[1]:
|
|
1156
1227
|
batch_voice = st.selectbox("Voz padrão", voice_options, format_func=lambda item: item or "Sem voz padrão", key="batch_channel_voice")
|
|
1157
1228
|
with defaults_cols[2]:
|
|
1158
|
-
batch_language = st.selectbox("Idioma",
|
|
1229
|
+
batch_language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=list(LANGUAGE_CODES).index("pt"), format_func=language_label, key="batch_channel_language")
|
|
1159
1230
|
with defaults_cols[3]:
|
|
1160
1231
|
batch_style = st.selectbox("Estilo wide", ["Pexels/Pixabay", "full_ia", "Apenas Música"], key="batch_channel_style")
|
|
1161
1232
|
import_selected = st.form_submit_button("Cadastrar canais seleccionados", type="primary", use_container_width=True)
|
|
@@ -1201,7 +1272,7 @@ def render_channels():
|
|
|
1201
1272
|
handle = st.text_input("Handle", placeholder="@seucanal", key="manual_channel_handle")
|
|
1202
1273
|
description = st.text_area("Descrição", key="manual_channel_description")
|
|
1203
1274
|
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",
|
|
1275
|
+
language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=list(LANGUAGE_CODES).index("pt"), format_func=language_label, key="manual_channel_language")
|
|
1205
1276
|
style = st.selectbox("Estilo wide", ["Pexels/Pixabay", "full_ia", "Apenas Música"], index=0, key="manual_channel_style")
|
|
1206
1277
|
manual_blueprint_items = blueprint_catalog()
|
|
1207
1278
|
manual_blueprint_ids = [item[0] for item in manual_blueprint_items]
|
|
@@ -1361,10 +1432,11 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1361
1432
|
)
|
|
1362
1433
|
mode = {"Canal específico": "single", "Lote no mesmo canal": "same_channel", "Lote geral": "general"}[mode_label]
|
|
1363
1434
|
selected_one: dict[str, Any] | None = None
|
|
1364
|
-
legacy_language = st.session_state.get("video_language")
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1435
|
+
legacy_language = st.session_state.get("video_language") or read_json("settings.json", {}).get("video_language")
|
|
1436
|
+
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)"}:
|
|
1437
|
+
st.session_state["video_language"] = "music"
|
|
1438
|
+
else:
|
|
1439
|
+
st.session_state["video_language"] = language_code(legacy_language or "pt")
|
|
1368
1440
|
generation_settings: dict[str, Any] = {}
|
|
1369
1441
|
if mode == "general":
|
|
1370
1442
|
selected = [str(channel["id"]) for channel in all_channels if channel.get("id")]
|
|
@@ -1625,6 +1697,7 @@ def render_new_video(page_title: str = "Criação de Vídeos"):
|
|
|
1625
1697
|
fmt = generation_settings["video_format"]
|
|
1626
1698
|
submitted = st.form_submit_button("Criar tarefas", type="primary")
|
|
1627
1699
|
if submitted:
|
|
1700
|
+
save_video_language(language)
|
|
1628
1701
|
style = {"Pexels/Pixabay": "pexels", "full_ia": "full_ia", "Apenas Música": "music"}[wide_style_label]
|
|
1629
1702
|
if style == "music" and not music_path:
|
|
1630
1703
|
st.error("Escolha, carregue ou gere uma música antes de criar o vídeo Apenas Música.")
|
|
@@ -1753,7 +1826,7 @@ def render_scripts():
|
|
|
1753
1826
|
legacy_script_language = str(st.session_state.get("script_language") or "")
|
|
1754
1827
|
script_settings = render_video_generation_settings(
|
|
1755
1828
|
"pipeline_scripts",
|
|
1756
|
-
current_language=legacy_script_language
|
|
1829
|
+
current_language=legacy_script_language or str(read_json("settings.json", {}).get("video_language") or "pt"),
|
|
1757
1830
|
)
|
|
1758
1831
|
language = script_settings["script_language"]
|
|
1759
1832
|
structure_notes = script_settings["script_structure_notes"]
|
|
@@ -4031,13 +4104,16 @@ def main():
|
|
|
4031
4104
|
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
4105
|
current_page = "Início"
|
|
4033
4106
|
st.session_state["page"] = current_page
|
|
4107
|
+
ui_language = current_ui_language()
|
|
4108
|
+
render_ui_language_picker(ui_language)
|
|
4034
4109
|
|
|
4035
4110
|
def navigate(target: str):
|
|
4036
4111
|
st.session_state["page"] = target
|
|
4037
4112
|
st.rerun()
|
|
4038
4113
|
|
|
4039
4114
|
def render_nav_button(target: str, icon: str, label: str, *, child: bool = False):
|
|
4040
|
-
|
|
4115
|
+
display_label = ui_text(label, ui_language)
|
|
4116
|
+
if st.button(display_label, key=f"nav_{target}", icon=icon, use_container_width=True, type="primary" if current_page == target else "secondary"):
|
|
4041
4117
|
navigate(target)
|
|
4042
4118
|
|
|
4043
4119
|
with st.sidebar:
|
|
@@ -4045,31 +4121,31 @@ def main():
|
|
|
4045
4121
|
st.markdown(f'<div class="tb-brand"><span class="tb-brand-name">Thunderbolt</span>{version_markup}</div>', unsafe_allow_html=True)
|
|
4046
4122
|
for target, icon, label in top_pages:
|
|
4047
4123
|
if target == "Pipeline":
|
|
4048
|
-
with st.expander("Pipeline", expanded=current_page in {item[0] for item in pipeline_items}, icon=":material/account_tree:"):
|
|
4124
|
+
with st.expander(ui_text("Pipeline", ui_language), expanded=current_page in {item[0] for item in pipeline_items}, icon=":material/account_tree:"):
|
|
4049
4125
|
for child_target, child_icon, child_label in pipeline_items:
|
|
4050
4126
|
render_nav_button(child_target, child_icon, child_label, child=True)
|
|
4051
4127
|
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:"):
|
|
4128
|
+
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
4129
|
for child_target, child_icon, child_label in pipeline_tiktok_items:
|
|
4054
4130
|
render_nav_button(child_target, child_icon, child_label, child=True)
|
|
4055
4131
|
elif target == "Automação":
|
|
4056
|
-
with st.expander("Automação", expanded=current_page in {item[0] for item in automation_items}, icon=":material/schedule:"):
|
|
4132
|
+
with st.expander(ui_text("Automação", ui_language), expanded=current_page in {item[0] for item in automation_items}, icon=":material/schedule:"):
|
|
4057
4133
|
for child_target, child_icon, child_label in automation_items:
|
|
4058
4134
|
render_nav_button(child_target, child_icon, child_label, child=True)
|
|
4059
4135
|
elif target == "Edição":
|
|
4060
|
-
with st.expander("Edição", expanded=current_page in {item[0] for item in edition_items}, icon=":material/edit:"):
|
|
4136
|
+
with st.expander(ui_text("Edição", ui_language), expanded=current_page in {item[0] for item in edition_items}, icon=":material/edit:"):
|
|
4061
4137
|
for child_target, child_icon, child_label in edition_items:
|
|
4062
4138
|
render_nav_button(child_target, child_icon, child_label, child=True)
|
|
4063
4139
|
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:"):
|
|
4140
|
+
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
4141
|
for child_target, child_icon, child_label in models_ai_items:
|
|
4066
4142
|
render_nav_button(child_target, child_icon, child_label, child=True)
|
|
4067
4143
|
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:"):
|
|
4144
|
+
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
4145
|
for child_target, child_icon, child_label in niche_finder_items:
|
|
4070
4146
|
render_nav_button(child_target, child_icon, child_label, child=True)
|
|
4071
4147
|
elif target == "Configurações":
|
|
4072
|
-
with st.expander("Configurações", expanded=current_page in {item[0] for item in settings_items}, icon=":material/settings:"):
|
|
4148
|
+
with st.expander(ui_text("Configurações", ui_language), expanded=current_page in {item[0] for item in settings_items}, icon=":material/settings:"):
|
|
4073
4149
|
for child_target, child_icon, child_label in settings_items:
|
|
4074
4150
|
render_nav_button(child_target, child_icon, child_label, child=True)
|
|
4075
4151
|
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