@danhachuel/thunderbolt 0.3.39 → 0.3.41
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 -4
- package/README.md +8 -2
- package/app/main.py +81 -20
- 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.3.
|
|
5
|
+
> **Versão deste manual:** 0.3.41
|
|
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)
|
|
@@ -100,13 +100,13 @@ Execute:
|
|
|
100
100
|
Windows PowerShell ou MobaXterm:
|
|
101
101
|
|
|
102
102
|
```powershell
|
|
103
|
-
npx.cmd --yes @danhachuel/thunderbolt@0.3.
|
|
103
|
+
npx.cmd --yes @danhachuel/thunderbolt@0.3.41 install
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
Linux/macOS:
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
|
-
npx --yes @danhachuel/thunderbolt@0.3.
|
|
109
|
+
npx --yes @danhachuel/thunderbolt@0.3.41 install
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
A instalação normal é **segura para actualizações**: preserva `storage`, Blueprints, Brandings, configurações e artefactos do utilizador. Remove apenas `.venv`, o clone técnico do MoneyPrinterTurbo e dependências que serão recriadas. Uma pasta antiga sem dados do utilizador, como `C:\Users\<utilizador>\AppData\Local\hermes` da tentativa incompleta, pode ser removida; uma pasta antiga que contenha Blueprints, Brandings ou storage é preservada e apenas avisada no terminal. Feche processos Python, Node, Streamlit e MobaXterm que estejam a usar as pastas antes de executar.
|
|
@@ -396,6 +396,10 @@ Uma execução da etapa Vídeo tem limite de 20 minutos. Se o processo externo t
|
|
|
396
396
|
|
|
397
397
|
Se o launcher ou o worker for encerrado abruptamente, uma tarefa `doing` sem actualização durante 25 minutos é recuperada e marcada como `failed`, evitando estados indefinidos eternos. Para processar novas tarefas, deixe a aplicação iniciada com o comando normal; o painel avisa quando não existe heartbeat recente do worker.
|
|
398
398
|
|
|
399
|
+
O **Backlog Vídeos** e a lista **Automação > Automação Youtube > Vídeos cadastrados** usam o mesmo catálogo completo de `storage/state/tasks.json`. Por isso, tarefas criadas manualmente e tarefas criadas pelo worker diário aparecem nos dois locais, sem filtragem implícita por origem. O filtro do Backlog mostra os estados conhecidos e inclui automaticamente estados adicionais encontrados nos dados.
|
|
400
|
+
|
|
401
|
+
Nos dois cards, o bloco de estado apresenta o valor técnico, o rótulo legível, a barra de progresso e a mensagem de erro quando existir. O bloco **Formato** resolve nesta ordem `format`, `style_wide`, `style` e, por fim, `wide`, mantendo a apresentação de formatos como `wide`, `shorts`, `music` ou `full_ia` mesmo em tarefas antigas.
|
|
402
|
+
|
|
399
403
|
### Upload Música — JewelMusic, Pushtunes e ytmusicapi
|
|
400
404
|
|
|
401
405
|
A área **Pipeline Música > Upload Música** separa três métodos com contratos diferentes. Em **JewelMusic**, active a integração, introduza a API Key fornecida pelo dashboard da JewelMusic e confirme a Base URL oficial `https://api.jewelmusic.com` e, se necessário, configure proxy e timeout. Carregue ou seleccione um ficheiro de música, indique artista e título e clique em **Enviar música para JewelMusic**. O teste de ligação consulta `/v1/ping`; o upload envia `multipart/form-data` para `/v1/tracks/upload` com os metadados preenchidos.
|
|
@@ -483,7 +487,7 @@ Ao abrir a página, o Thunderbolt não prepara dados públicos, não descarrega
|
|
|
483
487
|
|
|
484
488
|
Os parâmetros da UI são número de clusters entre 2 e 10, suporte mínimo entre 0,01 e 0,50, país, engagement, intervalo de datas e tags, todos dentro da área principal da aba. O núcleo normaliza os dados, calcula engagement, aplica filtros, faz transformação logarítmica e standardização, executa K-Means e calcula itemsets/regras com FP-Growth. Não são apresentados resultados até ao primeiro clique em **Analisar Nichos**; o mesmo botão aplica alterações posteriores aos filtros. Os resultados são DataFrames de clusters, itemsets frequentes, regras de associação e dados analisados; o gráfico de dispersão é criado nativamente com Plotly.
|
|
485
489
|
|
|
486
|
-
As dependências adicionais — `scikit-learn`, `mlxtend`, `plotly`, `seaborn`, `matplotlib` e `kagglehub` — são instaladas pelo procedimento normal de `npx`. Em instalações existentes, execute novamente `npx.cmd --yes @danhachuel/thunderbolt@0.3.
|
|
490
|
+
As dependências adicionais — `scikit-learn`, `mlxtend`, `plotly`, `seaborn`, `matplotlib` e `kagglehub` — são instaladas pelo procedimento normal de `npx`. Em instalações existentes, execute novamente `npx.cmd --yes @danhachuel/thunderbolt@0.3.41 install`; o instalador detecta e reutiliza o que já estiver válido.
|
|
487
491
|
|
|
488
492
|
### Niche Finder Apify
|
|
489
493
|
|
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ No topo da área principal da aplicação existe o menu nativo de idioma no padr
|
|
|
69
69
|
|
|
70
70
|
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` disponibiliza as variantes nomeadas **Dark** e **Light**, 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.
|
|
71
71
|
|
|
72
|
-
## Navegação da UI 0.3.
|
|
72
|
+
## Navegação da UI 0.3.41
|
|
73
73
|
|
|
74
74
|
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 Vídeos** é expansível e contém **Criação de Vídeos**, **Backlog Vídeos**, **Roteiros**, **Thumbnails** e **Upload**. **Pipeline Música** é expansível e contém **Criação de Músicas** e **Upload Música**. **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**, **Notificações**, **Logs** e **Configuração API**, nessa ordem. O Início reúne o dashboard e as filas do Pipeline, sem botões de acções rápidas.
|
|
75
75
|
|
|
@@ -89,6 +89,12 @@ A página **AI Influencers > Tutorial Meta** apresenta o guia de configuração
|
|
|
89
89
|
|
|
90
90
|
A página **Edição > Download Mídia** utiliza a API Python do [yt-dlp](https://github.com/yt-dlp/yt-dlp) para descarregar vídeos e áudio de URLs públicas, com qualidade, contentor, formato de áudio, legendas, metadados, playlists, progresso e histórico local em `storage/downloads/` e `storage/state/media_downloads.json`. Conversão e combinação de streams podem exigir FFmpeg.
|
|
91
91
|
|
|
92
|
+
## Catálogo comum do Backlog e da Automação
|
|
93
|
+
|
|
94
|
+
**Backlog Vídeos** e **Automação Youtube > Vídeos cadastrados** usam o mesmo catálogo completo persistido em `storage/state/tasks.json`. Assim, todas as tarefas criadas manualmente ou pela automação diária aparecem nos dois fluxos, sem duplicação nem filtros implícitos por origem. O filtro de estado do Backlog mantém os estados conhecidos e acrescenta estados novos encontrados no storage.
|
|
95
|
+
|
|
96
|
+
Os dois cards mostram de forma consistente o estado técnico, o rótulo legível, a barra de progresso, a etapa e o formato do vídeo. São suportados estados como `to_do`, `doing`, `blocked`, `done`, `failed` e `cancelled`; formatos ausentes usam `format`, `style_wide` ou `wide` como fallback.
|
|
97
|
+
|
|
92
98
|
## Canais Youtube — edição por cartão e vídeos recentes
|
|
93
99
|
|
|
94
100
|
A página **Canais Youtube** mantém o cadastro e a importação existentes, mas cada cartão agora tem o botão **Editar**. O editor permite alterar nome, URL, handle, idioma, estilo wide, **Nicho**, **Blueprint Padrão**, **Narrador/Voz Padrão**, conta Google do Upload directo, descrição e Automação ON/horário. O nicho aparece imediatamente abaixo do nome do canal no cartão; quando não existe, a UI mostra **SEM NICHO CONFIGURADO**.
|
|
@@ -101,7 +107,7 @@ Abaixo do cartão, a secção **Últimos 10 vídeos publicados** fica num expand
|
|
|
101
107
|
|
|
102
108
|
A aba **Criação de Vídeos** permite escrever manualmente o **Tópico ou briefing** ou usar **Gerar tópico/briefing com IA**. A geração usa o provider LLM configurado localmente em **Configuração API > API Keys > Serviços e modelos**, incorpora o Blueprint, a descrição, o idioma e a voz do canal e só ocorre depois do clique do utilizador. Se não houver credenciais ou modelo, a UI mostra uma mensagem accionável e não inventa conteúdo.
|
|
103
109
|
|
|
104
|
-
Entre **Canal** e **Estilo wide**, a UI mostra o Blueprint padrão resolvido do canal
|
|
110
|
+
Entre **Canal** e **Estilo wide**, a UI mostra o Blueprint padrão resolvido do canal, a voz associada e o **Idioma** configurado nesse canal. Ao trocar o canal, o selector **Script Language** é sincronizado automaticamente com o código canónico do novo canal, incluindo a conversão de rótulos legados como `Português` ou `36 – Português (Brasil)`. O valor sincronizado é usado no roteiro, keywords, geração criativa e payload da tarefa; o idioma global só serve como fallback quando não existe canal seleccionado. Quando não existe configuração, apresenta **SEM BLUEPRINT CONFIGURADO** e mantém o idioma do canal visível. Depois do briefing, **Gerar títulos e thumbnails com IA** cria pelo menos 20 candidatos de título e 3–5 variantes de thumbnail. Os títulos seguem as fórmulas e regras de curiosidade, especificidade, emoção, keyword inicial e humanização fornecidas nas referências empacotadas; as thumbnails guardam conceito, overlay de até quatro palavras, composição, cores, prompt e sinergia com o título.
|
|
105
111
|
|
|
106
112
|
O pacote criativo é persistido na task. O título escolhido fica pré-preenchido no Upload e a thumbnail é usada quando existe um ficheiro em `artifacts.thumbnail`. Sem provider de imagem configurado, a aplicação conserva o prompt e apresenta **Prompt de thumbnail pronto — imagem pendente de provider de imagem**, sem criar um PNG falso.
|
|
107
113
|
|
package/app/main.py
CHANGED
|
@@ -362,8 +362,13 @@ def save_ui_language(value: str) -> str:
|
|
|
362
362
|
return normalized
|
|
363
363
|
|
|
364
364
|
|
|
365
|
+
def normalize_video_language(value: Any, default: str = "pt") -> str:
|
|
366
|
+
raw = str(value or "").strip()
|
|
367
|
+
return "music" if raw.casefold() == "music" else language_code(raw, default=default)
|
|
368
|
+
|
|
369
|
+
|
|
365
370
|
def save_video_language(value: str) -> str:
|
|
366
|
-
normalized =
|
|
371
|
+
normalized = normalize_video_language(value)
|
|
367
372
|
settings = read_json("settings.json", {})
|
|
368
373
|
settings["video_language"] = normalized
|
|
369
374
|
write_json("settings.json", settings)
|
|
@@ -486,6 +491,13 @@ def blueprint_for_channel(channel: dict) -> dict[str, Any]:
|
|
|
486
491
|
return {"id": blueprint_id, "name": blueprint_id}
|
|
487
492
|
|
|
488
493
|
|
|
494
|
+
def channel_video_language(channel: dict[str, Any] | None, fallback: str = "pt") -> str:
|
|
495
|
+
"""Return the channel's canonical video language, preserving legacy labels."""
|
|
496
|
+
if not isinstance(channel, dict):
|
|
497
|
+
return normalize_video_language(fallback)
|
|
498
|
+
return normalize_video_language(channel.get("language") or fallback)
|
|
499
|
+
|
|
500
|
+
|
|
489
501
|
def channel_blueprint_summary(channel: dict) -> dict[str, str]:
|
|
490
502
|
blueprint = blueprint_for_channel(channel)
|
|
491
503
|
configured_id = str(channel.get("default_blueprint_id") or channel.get("blueprint_id") or "").strip()
|
|
@@ -501,12 +513,13 @@ def channel_blueprint_summary(channel: dict) -> dict[str, str]:
|
|
|
501
513
|
def render_channel_blueprint_panel(channel: dict, *, compact: bool = False) -> None:
|
|
502
514
|
summary = channel_blueprint_summary(channel)
|
|
503
515
|
voice = summary["voice"] or "Sem voz padrão"
|
|
516
|
+
video_language = language_label(channel_video_language(channel))
|
|
504
517
|
if summary["name"] == "SEM BLUEPRINT CONFIGURADO":
|
|
505
|
-
st.warning("**SEM BLUEPRINT CONFIGURADO** · configure um Blueprint padrão na aba Canais.")
|
|
518
|
+
st.warning(f"**SEM BLUEPRINT CONFIGURADO** · configure um Blueprint padrão na aba Canais. · **Idioma:** {video_language}")
|
|
506
519
|
elif compact:
|
|
507
|
-
st.caption(f"**Blueprint:** {summary['name']} · **Voz:** {voice}")
|
|
520
|
+
st.caption(f"**Blueprint:** {summary['name']} · **Voz:** {voice} · **Idioma:** {video_language}")
|
|
508
521
|
else:
|
|
509
|
-
st.info(f"**Blueprint utilizado pelo canal:** {summary['name']} · `{summary['id']}` · **Voz:** {voice}")
|
|
522
|
+
st.info(f"**Blueprint utilizado pelo canal:** {summary['name']} · `{summary['id']}` · **Voz:** {voice} · **Idioma:** {video_language}")
|
|
510
523
|
|
|
511
524
|
|
|
512
525
|
def creative_payload_from_result(channel: dict, topic: str, creative: dict, topic_source: str = "manual") -> dict[str, Any]:
|
|
@@ -746,7 +759,15 @@ def render_video_generation_settings(
|
|
|
746
759
|
key=f"{prefix}_video_subject",
|
|
747
760
|
placeholder="Ex.: How AI is changing everyday life",
|
|
748
761
|
)
|
|
749
|
-
normalized_current_language =
|
|
762
|
+
normalized_current_language = normalize_video_language(current_language)
|
|
763
|
+
if channel is not None:
|
|
764
|
+
channel_id = str(channel.get("id") or channel.get("name") or "")
|
|
765
|
+
channel_language_state_key = f"{prefix}_language_channel_id"
|
|
766
|
+
if st.session_state.get(channel_language_state_key) != channel_id:
|
|
767
|
+
channel_language = channel_video_language(channel, fallback=normalized_current_language)
|
|
768
|
+
st.session_state[f"{prefix}_script_language"] = channel_language
|
|
769
|
+
st.session_state[channel_language_state_key] = channel_id
|
|
770
|
+
normalized_current_language = normalize_video_language(st.session_state.get(f"{prefix}_script_language") or normalized_current_language)
|
|
750
771
|
settings["script_language"] = st.selectbox(
|
|
751
772
|
"Script Language",
|
|
752
773
|
VIDEO_LANGUAGE_SELECTION_OPTIONS,
|
|
@@ -3293,16 +3314,60 @@ def _render_pipeline_progress_panel() -> None:
|
|
|
3293
3314
|
_render_pipeline_progress_live()
|
|
3294
3315
|
|
|
3295
3316
|
|
|
3317
|
+
VIDEO_TASK_STATE_LABELS = {
|
|
3318
|
+
"to_do": "Pendente",
|
|
3319
|
+
"doing": "Em execução",
|
|
3320
|
+
"blocked": "Bloqueado",
|
|
3321
|
+
"done": "Concluído",
|
|
3322
|
+
"failed": "Falha",
|
|
3323
|
+
"cancelled": "Cancelado",
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
|
|
3327
|
+
def load_video_tasks_for_catalog() -> list[dict[str, Any]]:
|
|
3328
|
+
"""Return the complete persisted task catalog shared by Backlog and Automation."""
|
|
3329
|
+
saved = read_json("tasks.json", [])
|
|
3330
|
+
if not isinstance(saved, list):
|
|
3331
|
+
return []
|
|
3332
|
+
return [task for task in saved if isinstance(task, dict) and str(task.get("id") or "").strip()]
|
|
3333
|
+
|
|
3334
|
+
|
|
3335
|
+
def _video_task_format(task: dict[str, Any]) -> str:
|
|
3336
|
+
value = task.get("format") or task.get("style_wide") or task.get("style") or "wide"
|
|
3337
|
+
return str(value).strip() or "wide"
|
|
3338
|
+
|
|
3339
|
+
|
|
3340
|
+
def _video_task_progress(task: dict[str, Any]) -> int:
|
|
3341
|
+
try:
|
|
3342
|
+
return max(0, min(100, int(task.get("progress") or 0)))
|
|
3343
|
+
except (TypeError, ValueError):
|
|
3344
|
+
return 0
|
|
3345
|
+
|
|
3346
|
+
|
|
3347
|
+
def _render_video_task_state(task: dict[str, Any]) -> None:
|
|
3348
|
+
"""Show the raw state, readable label and progress consistently in both views."""
|
|
3349
|
+
state = str(task.get("state") or "unknown").strip().lower()
|
|
3350
|
+
progress = _video_task_progress(task)
|
|
3351
|
+
st.caption("Estado")
|
|
3352
|
+
st.write(state or "—")
|
|
3353
|
+
st.caption(VIDEO_TASK_STATE_LABELS.get(state, state.replace("_", " ").capitalize() or "Desconhecido"))
|
|
3354
|
+
st.progress(progress, text=f"{progress}%")
|
|
3355
|
+
if task.get("error"):
|
|
3356
|
+
st.caption(str(task.get("error"))[:240])
|
|
3357
|
+
|
|
3358
|
+
|
|
3296
3359
|
def render_videos():
|
|
3297
3360
|
st.subheader("Backlog Videos")
|
|
3298
3361
|
st.caption("Acompanhamento dos vídeos criados, estados da pipeline e controlos de execução.")
|
|
3299
3362
|
st.caption(f"Os vídeos são guardados em `{STORAGE / 'videos'}`.")
|
|
3300
3363
|
_render_pipeline_progress_panel()
|
|
3301
|
-
tasks =
|
|
3364
|
+
tasks = load_video_tasks_for_catalog()
|
|
3302
3365
|
if not tasks:
|
|
3303
3366
|
st.info("Nenhum vídeo criado.")
|
|
3304
3367
|
return
|
|
3305
|
-
|
|
3368
|
+
known_states = ["to_do", "doing", "blocked", "done", "failed", "cancelled"]
|
|
3369
|
+
extra_states = sorted({str(task.get("state") or "unknown") for task in tasks if str(task.get("state") or "unknown") not in known_states})
|
|
3370
|
+
state_filter = st.selectbox("Filtrar por estado", ["Todos", *known_states, *extra_states], key="videos_state_filter")
|
|
3306
3371
|
for task in tasks:
|
|
3307
3372
|
if state_filter != "Todos" and task.get("state") != state_filter:
|
|
3308
3373
|
continue
|
|
@@ -3319,16 +3384,13 @@ def render_videos():
|
|
|
3319
3384
|
status = task.get('thumbnail_status', 'not_generated')
|
|
3320
3385
|
prompt_note = ' · prompt pronto' if task.get('thumbnail_prompt') else ''
|
|
3321
3386
|
st.caption(f"Thumbnail: {status}{prompt_note}")
|
|
3322
|
-
with cols[1]:
|
|
3387
|
+
with cols[1]:
|
|
3388
|
+
st.caption("Formato")
|
|
3389
|
+
st.write(_video_task_format(task))
|
|
3323
3390
|
with cols[2]:
|
|
3324
3391
|
st.write(_pipeline_stage_label(task))
|
|
3325
|
-
if str(task.get("state") or "") in {"to_do", "doing", "blocked"}:
|
|
3326
|
-
progress = _pipeline_progress_value(task)
|
|
3327
|
-
st.progress(progress, text=f"{progress}%")
|
|
3328
3392
|
with cols[3]:
|
|
3329
|
-
|
|
3330
|
-
if task.get("error"):
|
|
3331
|
-
st.caption(str(task.get("error"))[:240])
|
|
3393
|
+
_render_video_task_state(task)
|
|
3332
3394
|
|
|
3333
3395
|
with cols[4]:
|
|
3334
3396
|
state = str(task.get("state") or "")
|
|
@@ -3615,21 +3677,20 @@ def render_automation():
|
|
|
3615
3677
|
|
|
3616
3678
|
st.divider()
|
|
3617
3679
|
st.subheader("Vídeos cadastrados")
|
|
3618
|
-
tasks =
|
|
3680
|
+
tasks = load_video_tasks_for_catalog()
|
|
3619
3681
|
if not tasks:
|
|
3620
3682
|
st.info("Ainda não existem vídeos cadastrados.")
|
|
3621
3683
|
for task in tasks:
|
|
3622
3684
|
with st.container(border=True):
|
|
3623
|
-
task_cols = st.columns([2.
|
|
3685
|
+
task_cols = st.columns([2.25, 1.65, 1.15, 1.85])
|
|
3624
3686
|
with task_cols[0]:
|
|
3625
3687
|
st.write(f"**{task.get('topic', 'Sem tópico')}**")
|
|
3626
3688
|
st.caption(f"{task.get('channel_name', 'Canal')} · {task.get('id', '')}")
|
|
3627
3689
|
with task_cols[1]:
|
|
3628
|
-
|
|
3629
|
-
st.write(task.get("state", "—"))
|
|
3690
|
+
_render_video_task_state(task)
|
|
3630
3691
|
with task_cols[2]:
|
|
3631
|
-
st.caption("
|
|
3632
|
-
st.write(task
|
|
3692
|
+
st.caption("Formato")
|
|
3693
|
+
st.write(_video_task_format(task))
|
|
3633
3694
|
with task_cols[3]:
|
|
3634
3695
|
state = str(task.get("state") or "")
|
|
3635
3696
|
start_col, stop_col = st.columns(2)
|
package/package.json
CHANGED