@danhachuel/thunderbolt 0.2.88 → 0.2.90

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.88
5
+ > **Versão deste manual:** 0.2.89
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)
@@ -200,7 +200,7 @@ Todas as subabas internas e o conteúdo das páginas também são traduzidos nos
200
200
 
201
201
  ### Temas Light e Dark
202
202
 
203
- A aplicação usa o mecanismo nativo de temas do Streamlit e é distribuída com `.streamlit/config.toml`, seguindo o padrão de configuração do [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo). O ficheiro define **Dark** como tema padrão e a UI mostra um selector explícito **Theme** com as opções **Dark** e **Light**. Se ainda não existir preferência, o Thunderbolt inicia sempre em Dark; quando o utilizador selecciona Light, essa escolha fica guardada em `storage/state/settings.json` como `ui_theme` e é restaurada nos arranques seguintes. Os componentes próprios da UI herdam as cores do tema activo através de variáveis semânticas, `currentColor` e `color-mix`. O toolbar nativo, o indicador de execução, **Deploy** e o menu principal continuam sem sobreposições CSS.
203
+ A aplicação usa o mecanismo nativo de temas do Streamlit e é distribuída com `.streamlit/config.toml`, seguindo o padrão de configuração do [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo). O ficheiro define **Dark** como base inicial. A alternância para **Light** e o regresso a **Dark** ficam exclusivamente no menu nativo de três pontos do Streamlit, no local original do toolbar; não existe um selector Theme dentro da página. Os componentes próprios da UI herdam as cores do tema activo através de `currentColor` e `color-mix`. O toolbar nativo, o indicador de execução, **Deploy** e o menu principal continuam sem sobreposições CSS.
204
204
 
205
205
  ## 4. Diagnóstico antes de iniciar
206
206
 
package/README.md CHANGED
@@ -54,9 +54,9 @@ No topo da área principal da aplicação existe o menu nativo de idioma no padr
54
54
 
55
55
  ## Temas claro e escuro
56
56
 
57
- A UI suporta os temas **Dark** e **Light** através de um selector explícito dentro da aplicação. **Dark é sempre o padrão** quando ainda não existe preferência guardada; o utilizador pode seleccionar **Light** no selector e essa escolha fica persistida em `storage/state/settings.json` como `ui_theme`. A configuração distribuída em `.streamlit/config.toml` também define `base = "dark"`, seguindo o padrão de configuração do [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo). O CSS próprio do Thunderbolt usa cores semânticas, `currentColor` e `color-mix` para que a sidebar, cartões, estados, radio cards, expanders e o hero de Cortes acompanhem o tema seleccionado. As cores de identidade dos chips de plataformas e das bandeiras são intencionais e não são substituídas pelo tema.
57
+ A UI suporta os temas **Dark** e **Light** através do menu nativo de três pontos do Streamlit, no local original do toolbar. Não existe um selector Theme adicional dentro da página. A configuração distribuída em `.streamlit/config.toml` define **Dark** como base inicial e o menu nativo continua responsável por alternar entre os modos, seguindo o padrão do [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo). O CSS próprio do Thunderbolt usa cores semânticas, `currentColor` e `color-mix` para acompanhar o tema activo, sem alterar a posição nem a funcionalidade do toolbar, do botão Deploy e do menu principal.
58
58
 
59
- ## Navegação da UI 0.2.88
59
+ ## Navegação da UI 0.2.89
60
60
 
61
61
  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**, **Tutorial Meta** e **Tutorial Supabase**, 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.
62
62
 
package/app/main.py CHANGED
@@ -22,7 +22,7 @@ except (OSError, json.JSONDecodeError):
22
22
 
23
23
  from hermes_ui.domain import STAGES, create_batch, create_channel, create_tasks_for_batch, delete_channel, pipeline_summary, set_channel_defaults, transition_task, update_channel, update_channel_video
24
24
  from hermes_ui.automation_worker import load_worker_status
25
- from hermes_ui.storage import BLUEPRINTS, MEDIA_DOWNLOADS, STORAGE, TIKTOK_PROMPT_MASTERS, ensure_storage, get_display_name, list_blueprint_files, list_prompt_master_files, load_blueprint_file, load_prompt_master_file, now, read_json, set_display_name, write_json
25
+ from hermes_ui.storage import BLUEPRINTS, DEFAULT_LLM_PROVIDER, MEDIA_DOWNLOADS, STORAGE, TIKTOK_PROMPT_MASTERS, ensure_storage, get_display_name, list_blueprint_files, list_prompt_master_files, load_blueprint_file, load_prompt_master_file, now, read_json, set_display_name, write_json
26
26
  from app.modules.niche_finder.apify import ApifyError, DEFAULT_ACTOR_ID, abort_actor_run, build_actor_input, get_dataset_items, normalize_video_items, start_actor_run, wait_for_actor_run
27
27
  from app.modules.niche_finder.core import NicheAnalysisError, run_niche_analysis
28
28
  from app.modules.niche_finder.data_loader import DatasetError, download_kaggle_dataset
@@ -144,70 +144,9 @@ BACKGROUND_MUSIC_VOLUME_OPTIONS = ["0%", "10%", "20%", "30%", "50%", "75%", "100
144
144
  SUBTITLE_FONT_OPTIONS = ["MicrosoftYaHeiBold.ttc", "Arial.ttf", "DejaVuSans.ttf"]
145
145
  SUBTITLE_POSITION_OPTIONS = ["Bottom (Recommended)", "Top", "Center"]
146
146
 
147
- UI_THEME_VALUES = ("dark", "light")
148
- UI_THEME_PALETTES = {
149
- "dark": {
150
- "background": "#0B1118",
151
- "sidebar": "#091018",
152
- "surface": "#121B26",
153
- "text": "#F4F8FB",
154
- "muted": "#9CAFBF",
155
- "border": "#2A4052",
156
- },
157
- "light": {
158
- "background": "#FFFFFF",
159
- "sidebar": "#F3F6FA",
160
- "surface": "#FFFFFF",
161
- "text": "#16202A",
162
- "muted": "#526273",
163
- "border": "#C9D2DC",
164
- },
165
- }
166
-
167
-
168
- def normalize_ui_theme(value: Any) -> str:
169
- return "light" if str(value or "").strip().casefold() in {"light", "claro", "clara", "浅色", "hell", "sáng", "açık", "светлая", "terang", "chiaro"} else "dark"
170
-
171
-
172
- def current_ui_theme() -> str:
173
- settings = read_json("settings.json", {})
174
- stored = st.session_state.get("ui_theme")
175
- if stored is None:
176
- stored = settings.get("ui_theme")
177
- normalized = normalize_ui_theme(stored) if stored is not None else "dark"
178
- st.session_state["ui_theme"] = normalized
179
- return normalized
180
-
181
-
182
-
183
- def save_ui_theme(value: str) -> str:
184
- normalized = normalize_ui_theme(value)
185
- settings = read_json("settings.json", {})
186
- settings["ui_theme"] = normalized
187
- write_json("settings.json", settings)
188
- st.session_state["ui_theme"] = normalized
189
- return normalized
190
-
191
-
192
147
  ensure_storage()
193
148
  st.set_page_config(page_title="Thunderbolt", page_icon="T", layout="wide", initial_sidebar_state="expanded")
194
149
 
195
- ui_theme = current_ui_theme()
196
- theme_palette = UI_THEME_PALETTES[ui_theme]
197
- st.markdown(
198
- f"""<style>
199
- :root {{
200
- --tb-theme-background: {theme_palette["background"]};
201
- --tb-theme-sidebar: {theme_palette["sidebar"]};
202
- --tb-theme-surface: {theme_palette["surface"]};
203
- --tb-theme-text: {theme_palette["text"]};
204
- --tb-theme-muted: {theme_palette["muted"]};
205
- --tb-theme-border: {theme_palette["border"]};
206
- color-scheme: {ui_theme};
207
- }}
208
- </style>""",
209
- unsafe_allow_html=True,
210
- )
211
150
  st.markdown("""
212
151
  <style>
213
152
  /*
@@ -216,32 +155,8 @@ st.markdown("""
216
155
  currentColor/color-mix e nunca uma paleta escura fixa.
217
156
  */
218
157
  :root { --tb-accent:#35a7ff; --tb-gold:#c59b55; }
219
- html, body { background:var(--tb-theme-background); color:var(--tb-theme-text); }
220
- [data-testid="stApp"] { background:var(--tb-theme-background); color:var(--tb-theme-text); }
221
- [data-testid="stHeader"] { background:var(--tb-theme-background); }
222
- [data-testid="stAppViewContainer"] { background:var(--tb-theme-background); color:var(--tb-theme-text); }
223
- [data-testid="stSidebar"] { background:var(--tb-theme-sidebar); color:var(--tb-theme-text); border-right:1px solid var(--tb-theme-border); }
224
- [data-testid="stTextInput"] input,
225
- [data-testid="stTextArea"] textarea,
226
- [data-testid="stNumberInput"] input,
227
- [data-testid="stDateInput"] input,
228
- [data-testid="stTimeInput"] input,
229
- [data-testid="stSelectbox"] [data-baseweb="select"] > div,
230
- [data-testid="stMultiSelect"] [data-baseweb="select"] > div,
231
- [data-testid="stColorPicker"] input { color:var(--tb-theme-text) !important; background:var(--tb-theme-surface) !important; border-color:var(--tb-theme-border) !important; }
232
- [data-testid="stFileUploader"] section,
233
- [data-testid="stAlert"],
234
- [data-testid="stDataFrame"],
235
- [data-testid="stTable"],
236
- [data-testid="stExpander"] details { color:var(--tb-theme-text); background:var(--tb-theme-surface); border-color:var(--tb-theme-border); }
237
- [data-testid="stButton"] button,
238
- [data-testid="stFormSubmitButton"] button { color:var(--tb-theme-text); background:var(--tb-theme-surface); border-color:var(--tb-theme-border); }
239
- [data-testid="stButton"] button:hover,
240
- [data-testid="stFormSubmitButton"] button:hover { color:var(--tb-theme-text); background:color-mix(in srgb, var(--tb-theme-text) 9%, var(--tb-theme-surface)); border-color:var(--tb-accent); }
241
- [data-testid="stTabs"] [data-baseweb="tab"],
242
- [data-testid="stTabs"] button { color:var(--tb-theme-text); }
243
- [data-testid="stSidebar"] [data-testid="stTextInput"] input,
244
- [data-testid="stSidebar"] [data-testid="stSelectbox"] [data-baseweb="select"] > div { background:var(--tb-theme-surface) !important; }
158
+ [data-testid="stAppViewContainer"] { background:transparent; color:inherit; }
159
+ [data-testid="stSidebar"] { background:color-mix(in srgb, currentColor 4%, transparent); border-right:1px solid color-mix(in srgb, currentColor 16%, transparent); }
245
160
  [data-testid="stSidebar"] .block-container { padding-top:0.28rem; padding-bottom:0.45rem; }
246
161
  [data-testid="stSidebar"] > div:first-child { padding-top:0.28rem; }
247
162
  [data-testid="stSidebar"] .tb-brand { display:flex; align-items:baseline; gap:0.42rem; margin:0 0 0.68rem 0; white-space:nowrap; }
@@ -368,29 +283,6 @@ def render_localized_tabs(labels: list[str], language: str | None = None):
368
283
  return st.tabs(localized_tab_labels(labels, language))
369
284
 
370
285
 
371
- def ui_theme_label(value: str, language: str | None = None) -> str:
372
- normalized = normalize_ui_theme(value)
373
- selected_language = language_code(language or current_ui_language())
374
- return ui_text("Dark" if normalized == "dark" else "Light", selected_language)
375
-
376
-
377
- def render_ui_theme_picker(language: str) -> None:
378
- """Render an explicit, persistent Light/Dark selector with Dark as the default."""
379
- selected_language = language_code(language)
380
- current = current_ui_theme()
381
- selected = st.radio(
382
- ui_text("Theme", selected_language),
383
- list(UI_THEME_VALUES),
384
- index=list(UI_THEME_VALUES).index(current),
385
- format_func=lambda value: ui_theme_label(value, selected_language),
386
- horizontal=True,
387
- key="top_ui_theme_selector",
388
- )
389
- if selected != current:
390
- save_ui_theme(selected)
391
- st.rerun()
392
-
393
-
394
286
  def save_ui_language(value: str) -> str:
395
287
  normalized = language_code(value)
396
288
  settings = read_json("settings.json", {})
@@ -435,9 +327,7 @@ def render_ui_language_picker(language: str) -> None:
435
327
  </style>''',
436
328
  unsafe_allow_html=True,
437
329
  )
438
- _spacer, theme_col, language_col = st.columns([3.6, 1.9, 1.5])
439
- with theme_col:
440
- render_ui_theme_picker(language)
330
+ _spacer, language_col = st.columns([5.2, 1.5])
441
331
  with language_col:
442
332
  selected = st.selectbox(
443
333
  "Language",
@@ -3676,8 +3566,9 @@ def render_settings():
3676
3566
  gemini_image_size = st.selectbox("Tamanho da imagem", ["1K", "2K", "4K"], index=["1K", "2K", "4K"].index(str(settings.get("gemini_image_size") or "1K")) if str(settings.get("gemini_image_size") or "1K") in {"1K", "2K", "4K"} else 0)
3677
3567
 
3678
3568
  with st.expander("LLM — providers e modelos", expanded=True):
3679
- provider_options = ["moonshot", "shengsuanyun", "openai", "gemini", "deepseek", "qwen", "azure", "volcengine", "grok", "minimax", "mimo", "cloudflare", "modelscope", "aihubmix", "aimlapi", "evolink", "ollama", "oneapi", "litellm", "groq", "pollinations"]
3680
- llm_provider = st.selectbox("LLM provider", provider_options, index=provider_options.index(settings.get("llm_provider", "moonshot")) if settings.get("llm_provider", "moonshot") in provider_options else 0)
3569
+ provider_options = ["openai", "moonshot", "shengsuanyun", "gemini", "deepseek", "qwen", "azure", "volcengine", "grok", "minimax", "mimo", "cloudflare", "modelscope", "aihubmix", "aimlapi", "evolink", "ollama", "oneapi", "litellm", "groq", "pollinations"]
3570
+ current_llm_provider = str(settings.get("llm_provider") or DEFAULT_LLM_PROVIDER).strip().lower()
3571
+ llm_provider = st.selectbox("LLM provider", provider_options, index=provider_options.index(current_llm_provider) if current_llm_provider in provider_options else 0)
3681
3572
  st.markdown("**OpenAI/ NVIDIA NIM — API key, Base URL e modelo**")
3682
3573
  st.caption("O provider interno continua a ser `openai`, mas pode usar qualquer endpoint OpenAI-compatible. Para NVIDIA NIM, a Base URL predefinida é `https://integrate.api.nvidia.com/v1`; o selector consulta `/models` e deixa um campo manual como fallback.")
3683
3574
  openai_cols = st.columns(3)
@@ -10,6 +10,8 @@ from typing import Any
10
10
 
11
11
  ROOT = Path(__file__).resolve().parents[1]
12
12
  STORAGE = Path(os.getenv("THUNDERBOLT_STORAGE_DIR") or ROOT / "storage")
13
+ DEFAULT_LLM_PROVIDER = "openai"
14
+ LEGACY_DEFAULT_LLM_PROVIDERS = {"", "moonshot"}
13
15
  STATE = STORAGE / "state"
14
16
  BLUEPRINTS = STORAGE / "blueprints"
15
17
  TIKTOK_PROMPT_MASTERS = STORAGE / "tiktok" / "prompts_master"
@@ -125,7 +127,7 @@ DEFAULTS: dict[str, Any] = {
125
127
  "apify_actor_id": "streamers~youtube-scraper",
126
128
  "apify_poll_interval_seconds": 10,
127
129
  "apify_run_timeout_seconds": 900,
128
- "llm_provider": "moonshot",
130
+ "llm_provider": DEFAULT_LLM_PROVIDER
129
131
  "moonshot_api_key": "",
130
132
  "moonshot_base_url": "",
131
133
  "moonshot_model_name": "",
@@ -290,6 +292,18 @@ def seed_prompt_masters() -> None:
290
292
  shutil.copy2(source, target)
291
293
 
292
294
 
295
+ def _migrate_settings(settings: Any) -> tuple[dict[str, Any], bool]:
296
+ """Keep the shipped OpenAI/NVIDIA NIM default across package upgrades."""
297
+ if not isinstance(settings, dict):
298
+ return {"llm_provider": DEFAULT_LLM_PROVIDER}, True
299
+
300
+ provider = str(settings.get("llm_provider") or "").strip().lower()
301
+ if provider not in LEGACY_DEFAULT_LLM_PROVIDERS:
302
+ return settings, False
303
+
304
+ return {**settings, "llm_provider": DEFAULT_LLM_PROVIDER}, True
305
+
306
+
293
307
  def ensure_storage() -> None:
294
308
  for path in [STATE, BLUEPRINTS / "canais", BLUEPRINTS / "nichos", BLUEPRINTS / "importados", BLUEPRINTS / "brandings", TIKTOK_PROMPT_MASTERS, MEDIA_DOWNLOADS, STORAGE / "brand", STORAGE / "scripts", STORAGE / "thumbnails", STORAGE / "videos", STORAGE / "artifacts", STORAGE / "skills", STORAGE / "metadata_cleaner", STORAGE / "metadata_cleaner" / "outputs", STORAGE / "music", STORAGE / "voice_previews", STORAGE / "python_editor", NICHES_DATA]:
295
309
  path.mkdir(parents=True, exist_ok=True)
@@ -321,7 +335,13 @@ def read_json(name: str, default: Any | None = None) -> Any:
321
335
  path = STATE / name
322
336
  try:
323
337
  with path.open("r", encoding="utf-8") as handle:
324
- return json.load(handle)
338
+ data = json.load(handle)
339
+ if name != "settings.json":
340
+ return data
341
+ migrated, changed = _migrate_settings(data)
342
+ if changed:
343
+ atomic_write(path, migrated)
344
+ return migrated
325
345
  except (json.JSONDecodeError, OSError):
326
346
  backup = path.with_suffix(path.suffix + f".corrupt-{datetime.now().strftime('%Y%m%d%H%M%S')}")
327
347
  if path.exists():
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.2.88",
3
+ "version": "0.2.90",
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",
@@ -302,6 +302,9 @@ function writeSettings(moneyprinterPath) {
302
302
  try { settings = JSON.parse(readFileSync(settingsPath, "utf8")); } catch { settings = {}; }
303
303
  }
304
304
  settings.moneyprinter_path = moneyprinterPath;
305
+ if (!settings.llm_provider || String(settings.llm_provider).trim().toLowerCase() === "moonshot") {
306
+ settings.llm_provider = "openai";
307
+ }
305
308
  settings.port = Number(process.env.THUNDERBOLT_PORT || process.env.HERMES_PORT || settings.port || 3030);
306
309
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
307
310
  }