@danhachuel/thunderbolt 0.3.61 → 0.3.63
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 +20 -6
- package/README.md +10 -2
- package/app/main.py +26 -6
- package/hermes_ui/automation_worker.py +6 -1
- package/hermes_ui/domain.py +14 -2
- package/hermes_ui/logs.py +43 -0
- package/hermes_ui/media_generation.py +5 -1
- package/hermes_ui/notifications.py +18 -1
- package/hermes_ui/pipeline_worker.py +418 -24
- package/package.json +1 -3
- package/seed/skills/mpt_agent.py +13 -3
- package/storage/blueprints/importados/demo-content-hermes.json +0 -23
- package/storage/tiktok/prompts_master/2Dstickman.md +0 -193
- package/storage/tiktok/prompts_master/3Dskeletoncharacter.md +0 -1
- package/storage/tiktok/prompts_master/ANIMALMOUNTEDCAMERA.md +0 -299
- package/storage/tiktok/prompts_master/ASMR.md +0 -74
- package/storage/tiktok/prompts_master/ASMRDEBOCACOMDOCES.md +0 -130
- package/storage/tiktok/prompts_master/AnimalX-Ray.md +0 -136
- package/storage/tiktok/prompts_master/CAREVOLUTION.md +0 -176
- package/storage/tiktok/prompts_master/CINEMATICBUNKERSURVIVAL.md +0 -180
- package/storage/tiktok/prompts_master/CINEMATICSLUMTRANSFORMATIONENGINE.md +0 -212
- package/storage/tiktok/prompts_master/CINEMATOGR/303/201FICOS3D.md +0 -207
- package/storage/tiktok/prompts_master/CartoonNostalgia.md +0 -131
- package/storage/tiktok/prompts_master/EPOXYFLOORTRANSFORMATION.md +0 -254
- package/storage/tiktok/prompts_master/Edgar'sHouseofTerrorsChooseYourStory.md +0 -134
- package/storage/tiktok/prompts_master/FOODBABYFACECHARACTER.md +0 -141
- package/storage/tiktok/prompts_master/FuitNovels.md +0 -166
- package/storage/tiktok/prompts_master/KIDSSTORYCREATOR(PROMODE).md +0 -235
- package/storage/tiktok/prompts_master/MatchstickPrompt.md +0 -86
- package/storage/tiktok/prompts_master/PERSONAGENSABANDONADOS.md +0 -208
- package/storage/tiktok/prompts_master/Pixar-styleanimated.md +0 -193
- package/storage/tiktok/prompts_master/RESTORATIONTIMELAPSE.md +0 -262
- package/storage/tiktok/prompts_master/RUSTREMOVAL.md +0 -275
- package/storage/tiktok/prompts_master/ReceitasIndianas.md +0 -60
- package/storage/tiktok/prompts_master/SkyscraperMonster.md +0 -140
- package/storage/tiktok/prompts_master/TIMETRAVELVLOG.md +0 -264
- package/storage/tiktok/prompts_master/Tarot.md +0 -58
- package/storage/tiktok/prompts_master/UFOs(OVNIs).md +0 -63
- package/storage/tiktok/prompts_master/VisualCraftSequenceGenerator.md +0 -284
- package/storage/tiktok/prompts_master/craftingStorytellingAI.md +0 -79
- package/storage/tiktok/prompts_master/hist/303/263riasinfantishoradedormir.md +0 -71
- package/storage/tiktok/prompts_master/viralTikTokdance.md +0 -77
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.63
|
|
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)
|
|
@@ -41,6 +41,20 @@ A geração guarda prompt, caption, provider, modelo, plataforma, estado e camin
|
|
|
41
41
|
|
|
42
42
|
O runtime do Thunderbolt adapta o comportamento dos workflows do episódio 35, mas não instala nem executa n8n. Formulários, Data Tables, Switch, Wait e subworkflows são substituídos por páginas Streamlit, repository Supabase/SQLite, adapters multimédia, worker, logs e notificações. Nunca coloque API keys nos JSONs dos workflows, em ficheiros versionados ou em mensagens de diagnóstico.
|
|
43
43
|
|
|
44
|
+
## Pipeline de vídeo: fontes, composição e ordem
|
|
45
|
+
|
|
46
|
+
Em **Pipeline Vídeos > Criação de Vídeos** e **Automação Youtube**, seleccione a fonte no próprio vídeo ou na automação. **Pexels/Pixabay** é a rota stock e usa a infraestrutura do [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo): pesquisa por keywords, filtros de proporção e duração, cache e deduplicação de clips, descarga de materiais e composição MoviePy/FFmpeg com concatenação, transições, narração, legendas e música de fundo. A fonte escolhida é passada explicitamente ao processo (`pexels` ou `pixabay`) e as listas de API keys são sincronizadas no `config.toml`, permitindo a rotação configurada.
|
|
47
|
+
|
|
48
|
+
A execução segue sempre **Tema → Script → Título → Keywords (opcional) → Vídeo → Prompt Thumbnail (JSON) → Thumbnail (imagem) → Upload**. Keywords são opcionais: se a geração por LLM falhar, é usado um fallback determinístico para não bloquear a etapa de vídeo. O worker guarda o MP4 antes de iniciar prompt ou imagem da thumbnail; assim, uma falha de quota na imagem não elimina um vídeo concluído.
|
|
49
|
+
|
|
50
|
+
| Opção visível | Rota executada | Resultado esperado |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| Pexels/Pixabay | MoneyPrinterTurbo stock com a fonte efectiva e as chaves correspondentes | MP4 composto localmente |
|
|
53
|
+
| Full IA | Pool de vídeo limitado a FAL AI, KIE AI e Agnes AI | MP4 gerado pelo provider activo |
|
|
54
|
+
| Apenas Música | Áudio local/Suno já preparado | Ficheiro musical pronto para um upload de música; não gera MP4, thumbnail nem upload YouTube |
|
|
55
|
+
|
|
56
|
+
Antes de iniciar a rota stock, confirme em **Configurações > Configuração API > Fontes de materiais** que existe pelo menos uma API key para a fonte efectiva. Sem uma key Pexels/Pixabay, o erro é apresentado como configuração da fonte, em vez de uma falha genérica de vídeo. Se o MoneyPrinterTurbo devolver `MPT_NEEDS_INPUT`, o Thunderbolt lê `LLM_PROVIDER`, `MISSING` e `INVALID` e mostra no erro todas as APIs afectadas, como **OpenAI / NVIDIA NIM API + Pexels API**, incluindo os campos que faltam. **Google Lyria não é apresentado como implementado nesta release**; a rota Apenas Música aceita áudio local e a integração Suno existente.
|
|
57
|
+
|
|
44
58
|
## 2. Pré-requisitos
|
|
45
59
|
|
|
46
60
|
Instale os seguintes componentes antes de executar o instalador:
|
|
@@ -114,13 +128,13 @@ Execute:
|
|
|
114
128
|
Windows PowerShell ou MobaXterm:
|
|
115
129
|
|
|
116
130
|
```powershell
|
|
117
|
-
npx.cmd --yes --prefer-online @danhachuel/thunderbolt@0.3.
|
|
131
|
+
npx.cmd --yes --prefer-online @danhachuel/thunderbolt@0.3.63 install
|
|
118
132
|
```
|
|
119
133
|
|
|
120
134
|
Linux/macOS:
|
|
121
135
|
|
|
122
136
|
```bash
|
|
123
|
-
npx --yes --prefer-online @danhachuel/thunderbolt@0.3.
|
|
137
|
+
npx --yes --prefer-online @danhachuel/thunderbolt@0.3.63 install
|
|
124
138
|
```
|
|
125
139
|
|
|
126
140
|
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.
|
|
@@ -128,7 +142,7 @@ A instalação normal é **segura para actualizações**: preserva `storage`, Bl
|
|
|
128
142
|
Se quiser apagar absolutamente tudo de forma intencional, use o comando destrutivo separado:
|
|
129
143
|
|
|
130
144
|
```powershell
|
|
131
|
-
npx.cmd --yes --prefer-online @danhachuel/thunderbolt@0.3.
|
|
145
|
+
npx.cmd --yes --prefer-online @danhachuel/thunderbolt@0.3.63 install --purge-data
|
|
132
146
|
```
|
|
133
147
|
|
|
134
148
|
O parâmetro `--purge-data` apaga Blueprints, Brandings, configurações, storage e artefactos locais. Não o use numa actualização normal.
|
|
@@ -524,7 +538,7 @@ O cartão mostra o nicho imediatamente abaixo do nome. Os quatro blocos compacto
|
|
|
524
538
|
|
|
525
539
|
A secção **Últimos 10 vídeos publicados** fica abaixo das configurações do canal, dentro de um expander fechado por defeito, e não dentro de Criação de Vídeos. Clique no expander e depois em **Actualizar últimos 10 vídeos** para consultar o feed RSS público do YouTube sem Data API Key. Os resultados são guardados em `storage/state/channel_videos.json`. A única vista disponível é **Lista**, que apresenta título, data, URL, estado e botão **Editar vídeo**. A edição local permite alterar título, estado, data, URL e notas. Essas alterações são overrides de gestão local e não publicam automaticamente no YouTube.
|
|
526
540
|
|
|
527
|
-
A página **Configurações > Logs** aparece no menu entre **Notificações** e **Configuração API**. Mostra uma projecção unificada das tarefas e notificações persistentes, com filtro livre por operação, filtros por operação e estado e as colunas mínimas **Operação**, **Estado**, **Data** e **Hora**. A tabela também mostra **Registo**, **Origem**, **Progresso** e **Detalhes**;
|
|
541
|
+
A página **Configurações > Logs** aparece no menu entre **Notificações** e **Configuração API**. Mostra uma projecção unificada das tarefas e notificações persistentes, com filtro livre por operação, filtros por operação e estado e as colunas mínimas **Operação**, **Estado**, **Data** e **Hora**. A tabela também mostra **Registo**, **Origem**, **Progresso**, **API/Provider** e **Detalhes**; em qualquer falha nova, **API/Provider** identifica a API ou o pool responsável, o serviço, a rota e os campos de configuração em falta. Registos anteriores sem essa metadata aparecem explicitamente como falhas históricas cuja API não pôde ser identificada. Estados como pendente, em execução, concluído, publicado, falha, cancelado e bloqueado são apresentados quando existirem no storage local.
|
|
528
542
|
|
|
529
543
|
## Canais em lote por conta Google/YouTube
|
|
530
544
|
|
|
@@ -542,7 +556,7 @@ Ao abrir a página, o Thunderbolt não prepara dados públicos, não descarrega
|
|
|
542
556
|
|
|
543
557
|
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.
|
|
544
558
|
|
|
545
|
-
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 --prefer-online @danhachuel/thunderbolt@0.3.
|
|
559
|
+
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 --prefer-online @danhachuel/thunderbolt@0.3.63 install`; o instalador detecta e reutiliza o que já estiver válido.
|
|
546
560
|
|
|
547
561
|
### Niche Finder Apify
|
|
548
562
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Consulte o [Manual completo de instalação](MANUAL-INSTALACAO.md) antes do primeiro teste local.
|
|
4
4
|
|
|
5
|
-
Thunderbolt — UI web local da Fase 3, baseada no fluxo Streamlit do [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo). A aplicação organiza canais, blueprints, lotes de vídeos, filas, artefactos e upload em armazenamento local JSON.
|
|
5
|
+
Thunderbolt — UI web local da Fase 3, baseada no fluxo Streamlit do [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo). A aplicação organiza canais, blueprints, lotes de vídeos, filas, artefactos e upload em armazenamento local JSON. O pacote npm distribui apenas os seeds versionados em `seed/`; não inclui `storage/` persistente, pelo que Blueprints, personagens, configurações e artefactos locais não são enviados durante uma actualização.
|
|
6
6
|
|
|
7
7
|
## Estado actual
|
|
8
8
|
|
|
@@ -38,6 +38,14 @@ A página **AI Influencers > Geração de Conteúdo IA** contém as subabas **Im
|
|
|
38
38
|
|
|
39
39
|
A migração é baseada no workflow público [AI Agents A-Z — episódio 35](https://github.com/gyoridavid/ai_agents_az/tree/main/episode_35), mas o runtime do Thunderbolt não instala nem executa n8n. Os formulários, Data Tables, waits e subworkflows são substituídos pela UI Streamlit, pelo repositório dual Supabase/SQLite, pelos adapters multimédia e pelos logs/notificações locais. As API keys são sempre configuradas localmente e não devem ser colocadas nos JSONs do workflow ou no GitHub.
|
|
40
40
|
|
|
41
|
+
## Pipeline de vídeo — fontes e ordem de execução
|
|
42
|
+
|
|
43
|
+
Em **Pipeline Vídeos > Criação de Vídeos** e **Automação Youtube**, a opção **Pexels/Pixabay** usa a rota stock do [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo): as keywords do roteiro são encaminhadas para a pesquisa da fonte seleccionada, os clips são descarregados e reutilizados localmente, e o motor faz a composição com MoviePy/FFmpeg, respeitando proporção, duração máxima, concatenação, transições, correspondência visual ao roteiro, narração, legendas e música de fundo. As API keys de Pexels e Pixabay são exportadas para o `config.toml` do motor e a fonte efectiva é encaminhada por tarefa, sem depender apenas da fonte global guardada nas configurações.
|
|
44
|
+
|
|
45
|
+
A ordem persistida da criação é **Tema → Script → Título → Keywords opcional → Vídeo → Prompt Thumbnail em JSON → Thumbnail → Upload**. O vídeo é materializado antes do prompt e da imagem da thumbnail; uma falha posterior de thumbnail não invalida um MP4 já pronto. **Full IA** é uma rota separada e usa exclusivamente os cartões activos FAL AI, KIE AI e Agnes AI do pool de vídeo. **Apenas Música** não chama a pipeline de vídeo nem tenta gerar thumbnail: reutiliza o áudio local/Suno já descarregado e deixa-o pronto para a integração de upload musical.
|
|
46
|
+
|
|
47
|
+
Quando uma etapa falha, a tarefa, a notificação e a página **Configurações > Logs** guardam e mostram sempre a coluna **API/Provider**, o serviço, a rota e, quando aplicável, os campos de configuração em falta. No caso do MoneyPrinterTurbo, os marcadores `LLM_PROVIDER`, `MISSING` e `INVALID` são convertidos em attribution legível; por exemplo, um erro pode indicar simultaneamente **OpenAI / NVIDIA NIM API** e **Pexels API**, em vez de apresentar apenas a mensagem genérica de credenciais adicionais. Registos históricos sem metadata são identificados explicitamente como anteriores à attribution estruturada.
|
|
48
|
+
|
|
41
49
|
## Upload-Post — publicação para múltiplas plataformas
|
|
42
50
|
|
|
43
51
|
A página **Upload** contém quatro subabas: **Upload convencional**, **Upload directo**, **Postiz** e **Upload-Post**. A quarta alternativa usa a API oficial do [Upload-Post](https://docs.upload-post.com/) para enviar um vídeo local para uma ou mais plataformas ligadas ao perfil configurado. Seleccione as plataformas na própria subaba, confirme o título e a descrição e clique em **Enviar vídeo pelo Upload-Post**.
|
|
@@ -79,7 +87,7 @@ No topo da área principal da aplicação existe o menu nativo de idioma no padr
|
|
|
79
87
|
|
|
80
88
|
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.
|
|
81
89
|
|
|
82
|
-
## Navegação da UI 0.3.
|
|
90
|
+
## Navegação da UI 0.3.63
|
|
83
91
|
|
|
84
92
|
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**, **Geração de Conteúdo IA**, **Redes Sociais**, **Tutorial Meta** e **Tutorial Supabase**, nessa ordem. A página Geração de Conteúdo IA divide-se em **Imagens**, **Vídeos** e **Motion Control**; a subaba Motion Control permanece vazia nesta release.
|
|
85
93
|
**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.
|
package/app/main.py
CHANGED
|
@@ -97,6 +97,7 @@ AI_STYLE_OPTIONS = [
|
|
|
97
97
|
]
|
|
98
98
|
|
|
99
99
|
WIDE_STYLE_OPTIONS = ["Pexels/Pixabay", "full_ia", "Apenas Música"]
|
|
100
|
+
MATERIAL_SOURCE_OPTIONS = ["Pexels", "Pixabay"]
|
|
100
101
|
|
|
101
102
|
VIDEO_LANGUAGE_OPTIONS = [
|
|
102
103
|
"00 – Apenas Música de Fundo (Sem Falas)",
|
|
@@ -993,6 +994,18 @@ def render_video_generation_settings(
|
|
|
993
994
|
video_cols = st.columns(2)
|
|
994
995
|
with video_cols[0]:
|
|
995
996
|
settings["video_source"] = st.selectbox("Video Source", WIDE_STYLE_OPTIONS, key=f"{prefix}_video_source")
|
|
997
|
+
if settings["video_source"] == "Pexels/Pixabay":
|
|
998
|
+
configured_material_source = selected_material_source(read_json("settings.json", {}))
|
|
999
|
+
material_source_labels = {"pexels": "Pexels", "pixabay": "Pixabay"}
|
|
1000
|
+
configured_material_label = material_source_labels.get(configured_material_source, "Pexels")
|
|
1001
|
+
settings["material_source"] = st.selectbox(
|
|
1002
|
+
"Stock Material Source",
|
|
1003
|
+
MATERIAL_SOURCE_OPTIONS,
|
|
1004
|
+
index=MATERIAL_SOURCE_OPTIONS.index(configured_material_label),
|
|
1005
|
+
key=f"{prefix}_material_source",
|
|
1006
|
+
)
|
|
1007
|
+
else:
|
|
1008
|
+
settings["material_source"] = ""
|
|
996
1009
|
if settings["video_source"] == "full_ia":
|
|
997
1010
|
settings["style_ia"] = st.selectbox("Estilo IA", AI_STYLE_OPTIONS, key=f"{prefix}_style_ia")
|
|
998
1011
|
else:
|
|
@@ -2037,6 +2050,7 @@ def _create_video_task_from_saved_script(record: dict[str, Any], channel: dict[s
|
|
|
2037
2050
|
"format": settings.get("video_format", "wide"),
|
|
2038
2051
|
"style_wide": style,
|
|
2039
2052
|
"style_ia": settings.get("style_ia", ""),
|
|
2053
|
+
"material_source": settings.get("material_source", "") if style == "pexels" else "",
|
|
2040
2054
|
"music_mode": style == "music",
|
|
2041
2055
|
"background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"),
|
|
2042
2056
|
"voice": str(settings.get("voice") or channel.get("default_voice") or channel.get("voice") or ""),
|
|
@@ -2266,6 +2280,12 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
2266
2280
|
),
|
|
2267
2281
|
)
|
|
2268
2282
|
wide_style_label = generation_settings["video_source"]
|
|
2283
|
+
style = {"Pexels/Pixabay": "pexels", "full_ia": "full_ia", "Apenas Música": "music"}[wide_style_label]
|
|
2284
|
+
material_source = (
|
|
2285
|
+
{"Pexels": "pexels", "Pixabay": "pixabay"}.get(str(generation_settings.get("material_source") or ""), "")
|
|
2286
|
+
if style == "pexels"
|
|
2287
|
+
else ""
|
|
2288
|
+
)
|
|
2269
2289
|
style_ia = generation_settings.get("style_ia", "")
|
|
2270
2290
|
music_path = ""
|
|
2271
2291
|
music_source = ""
|
|
@@ -2469,7 +2489,6 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
2469
2489
|
submitted = st.form_submit_button("Criar tarefas", type="primary")
|
|
2470
2490
|
if submitted:
|
|
2471
2491
|
save_video_language(language)
|
|
2472
|
-
style = {"Pexels/Pixabay": "pexels", "full_ia": "full_ia", "Apenas Música": "music"}[wide_style_label]
|
|
2473
2492
|
if style == "music" and not music_path:
|
|
2474
2493
|
st.error("Escolha, carregue ou gere uma música antes de criar o vídeo Apenas Música.")
|
|
2475
2494
|
st.stop()
|
|
@@ -2517,8 +2536,8 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
2517
2536
|
st.session_state[f"{prefix}_general_topics"] = {cid: {"topic": payload["topic"], "topic_source": payload.get("topic_source", "llm")} for cid, payload in payloads.items()}
|
|
2518
2537
|
if len(payloads) == len(selected):
|
|
2519
2538
|
batch_topic = "Lote geral — um vídeo independente por canal"
|
|
2520
|
-
channel_payloads = {cid: {**payload, "language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source, "generation_settings": generation_settings} for cid, payload in payloads.items()}
|
|
2521
|
-
batch = create_batch("general", selected, batch_topic, 1, {"language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source, "generation_settings": generation_settings, "topic_source": "llm", "channel_payloads": channel_payloads})
|
|
2539
|
+
channel_payloads = {cid: {**payload, "language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "material_source": material_source, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source, "generation_settings": generation_settings} for cid, payload in payloads.items()}
|
|
2540
|
+
batch = create_batch("general", selected, batch_topic, 1, {"language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "material_source": material_source, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source, "generation_settings": generation_settings, "topic_source": "llm", "channel_payloads": channel_payloads})
|
|
2522
2541
|
tasks = create_tasks_for_batch(batch)
|
|
2523
2542
|
st.success(f"Lote geral {batch['id']} criado com {len(tasks)} tarefas independentes, uma por canal.")
|
|
2524
2543
|
else:
|
|
@@ -2542,8 +2561,8 @@ def render_new_video(page_title: str = "Criação de Vídeos", prefix: str = "ne
|
|
|
2542
2561
|
except CreativeGenerationError as exc:
|
|
2543
2562
|
st.warning(f"Título/keywords automáticos pendentes: {exc} A tarefa será criada com o tópico como título; o prompt e a imagem da thumbnail serão gerados depois do vídeo.")
|
|
2544
2563
|
payload = {"topic": topic_value, "title": topic_value, "topic_source": "manual", "thumbnail_status": "pending_provider", "thumbnail_variants": [], "thumbnail_variant": {}, "thumbnail_prompt": "", "thumbnail_text": ""}
|
|
2545
|
-
payload.update({"topic": topic_value, "topic_source": payload.get("topic_source") or _video_topic_source(topic_value), "language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source, "generation_settings": generation_settings})
|
|
2546
|
-
batch = create_batch(mode, selected, topic_value, quantity_value, {"language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source, "generation_settings": generation_settings, "topic_source": payload.get("topic_source", "manual"), "channel_payloads": {selected[0]: payload}})
|
|
2564
|
+
payload.update({"topic": topic_value, "topic_source": payload.get("topic_source") or _video_topic_source(topic_value), "language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "material_source": material_source, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source, "generation_settings": generation_settings})
|
|
2565
|
+
batch = create_batch(mode, selected, topic_value, quantity_value, {"language": language, "format": fmt, "style_wide": style, "style_ia": style_ia, "material_source": material_source, "music_mode": style == "music", "background_mode": "none" if style == "music" else ("ai" if style == "full_ia" else "stock"), "music_path": music_path, "music_source": music_source, "generation_settings": generation_settings, "topic_source": payload.get("topic_source", "manual"), "channel_payloads": {selected[0]: payload}})
|
|
2547
2566
|
tasks = create_tasks_for_batch(batch)
|
|
2548
2567
|
st.success(f"Lote {batch['id']} criado com {len(tasks)} tarefa(s). Abra {ui_text('Backlog Vídeos', current_ui_language())} para acompanhar.")
|
|
2549
2568
|
|
|
@@ -5843,10 +5862,11 @@ def render_logs():
|
|
|
5843
5862
|
"Registo": st.column_config.TextColumn("Registo", width=280),
|
|
5844
5863
|
"Origem": st.column_config.TextColumn("Origem", width=130),
|
|
5845
5864
|
"Progresso": st.column_config.TextColumn("Progresso", width=100),
|
|
5865
|
+
"API/Provider": st.column_config.TextColumn("API/Provider", width=220),
|
|
5846
5866
|
"Detalhes": st.column_config.TextColumn("Detalhes", width=760),
|
|
5847
5867
|
},
|
|
5848
5868
|
)
|
|
5849
|
-
st.caption("Quando a tabela exceder a largura disponível, utilize a barra de rolagem horizontal na parte inferior para consultar todo o conteúdo das células.")
|
|
5869
|
+
st.caption("A coluna API/Provider identifica a API responsável por cada falha; quando o registo é anterior a esta correcção, o sistema assinala que a API não pôde ser identificada. Quando a tabela exceder a largura disponível, utilize a barra de rolagem horizontal na parte inferior para consultar todo o conteúdo das células.")
|
|
5850
5870
|
|
|
5851
5871
|
|
|
5852
5872
|
def render_notifications():
|
|
@@ -10,6 +10,7 @@ from typing import Any
|
|
|
10
10
|
from . import storage
|
|
11
11
|
from .creative_generation import generate_title_and_keywords, generate_topic_for_channel
|
|
12
12
|
from .domain import create_batch, create_tasks_for_batch
|
|
13
|
+
from .material_sources import selected_material_source
|
|
13
14
|
from .notifications import record_notification
|
|
14
15
|
|
|
15
16
|
WORKER_STATE_FILE = "automation_worker.json"
|
|
@@ -156,6 +157,8 @@ def _creative_payload(channel: dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
|
|
156
157
|
blueprint,
|
|
157
158
|
language=str(channel.get("language") or "Português"),
|
|
158
159
|
)
|
|
160
|
+
style_wide = str(channel.get("style_wide") or "pexels").strip().casefold()
|
|
161
|
+
material_source = selected_material_source(settings) if style_wide in {"pexels", "pexels/pixabay", "stock"} else ""
|
|
159
162
|
payload = {
|
|
160
163
|
"topic": topic_package["topic"],
|
|
161
164
|
"topic_source": "llm",
|
|
@@ -169,7 +172,8 @@ def _creative_payload(channel: dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
|
|
169
172
|
"blueprint_id": str(channel.get("default_blueprint_id") or channel.get("blueprint_id") or ""),
|
|
170
173
|
"blueprint_name": str(blueprint.get("name") or "SEM BLUEPRINT CONFIGURADO"),
|
|
171
174
|
"voice": str(channel.get("default_voice") or channel.get("voice") or ""),
|
|
172
|
-
"
|
|
175
|
+
"material_source": material_source,
|
|
176
|
+
"generation_settings": {"video_keywords": editorial.get("keywords", []), "material_source": material_source},
|
|
173
177
|
"ai_generation": {"topic": topic_package, "editorial": editorial},
|
|
174
178
|
}
|
|
175
179
|
return topic_package["topic"], payload
|
|
@@ -204,6 +208,7 @@ def _create_channel_batch(channel: dict[str, Any], when: datetime) -> dict[str,
|
|
|
204
208
|
"format": "wide",
|
|
205
209
|
"style_wide": style_wide,
|
|
206
210
|
"style_ia": channel.get("style_ia") or "",
|
|
211
|
+
"material_source": payload.get("material_source") or "",
|
|
207
212
|
"music_mode": music_mode,
|
|
208
213
|
"background_mode": "none" if music_mode else ("ai" if style_wide == "full_ia" else "stock"),
|
|
209
214
|
"music_path": channel.get("music_path") or "",
|
package/hermes_ui/domain.py
CHANGED
|
@@ -160,6 +160,7 @@ def create_tasks_for_batch(batch: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
160
160
|
"language": payload.get("language", options.get("language", channel.get("language", "Português"))),
|
|
161
161
|
"format": payload.get("format", options.get("format", "wide")),
|
|
162
162
|
"style_wide": payload.get("style_wide", options.get("style_wide", channel.get("style_wide", "pexels"))),
|
|
163
|
+
"material_source": payload.get("material_source", options.get("material_source", channel.get("material_source", ""))),
|
|
163
164
|
"style_ia": payload.get("style_ia", options.get("style_ia", "")),
|
|
164
165
|
"music_mode": payload.get("music_mode", options.get("music_mode", False)),
|
|
165
166
|
"music_path": payload.get("music_path", options.get("music_path", "")),
|
|
@@ -220,11 +221,22 @@ def _notify_task_completion(task: dict[str, Any], previous_state: str = "") -> N
|
|
|
220
221
|
title = str(task.get("title") or task.get("topic") or "Actividade")
|
|
221
222
|
channel = str(task.get("channel_name") or "Canal")
|
|
222
223
|
if current_state == "failed":
|
|
224
|
+
failure_api = str(task.get("failure_api") or "API não identificada (falha anterior)").strip()
|
|
223
225
|
record_notification(
|
|
224
226
|
"activity_failed",
|
|
225
227
|
f"Actividade falhou: {title}",
|
|
226
|
-
f"A actividade de {channel} terminou com erro.",
|
|
227
|
-
metadata={
|
|
228
|
+
f"A actividade de {channel} terminou com erro. API/provider: {failure_api}.",
|
|
229
|
+
metadata={
|
|
230
|
+
"task_id": task_id,
|
|
231
|
+
"channel_name": channel,
|
|
232
|
+
"error": task.get("error") or "",
|
|
233
|
+
"failure_api": task.get("failure_api") or "API não identificada (falha anterior)",
|
|
234
|
+
"failure_provider": task.get("failure_provider") or "unknown",
|
|
235
|
+
"failure_service": task.get("failure_service") or "Thunderbolt",
|
|
236
|
+
"failure_route": task.get("failure_route") or "",
|
|
237
|
+
"failure_config_fields": task.get("failure_config_fields") or "",
|
|
238
|
+
"failure_stage": task.get("failure_stage") or task.get("failed_stage") or task.get("stage") or "pipeline",
|
|
239
|
+
},
|
|
228
240
|
dedupe_key=f"task:{task_id}:failed",
|
|
229
241
|
)
|
|
230
242
|
return
|
package/hermes_ui/logs.py
CHANGED
|
@@ -76,6 +76,29 @@ def _operation_label(operation_code: str, fallback: str = "Operação") -> str:
|
|
|
76
76
|
return str(event.get("label") if event else fallback or operation_code or "Operação")
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
def _fallback_task_failure_api(task: dict[str, Any]) -> str:
|
|
80
|
+
"""Infer a safe API label for legacy failed tasks without structured metadata."""
|
|
81
|
+
route = str(task.get("material_source") or task.get("style_wide") or "").strip().casefold()
|
|
82
|
+
if "pexels" in route and "pixabay" in route:
|
|
83
|
+
return "Pexels/Pixabay API (fonte efectiva não registada)"
|
|
84
|
+
if "pixabay" in route:
|
|
85
|
+
return "Pixabay API"
|
|
86
|
+
if "pexels" in route or "stock" in route:
|
|
87
|
+
return "Pexels/Pixabay API (fonte efectiva não registada)"
|
|
88
|
+
if route in {"full_ia", "full ia", "ia"}:
|
|
89
|
+
return "FAL AI / KIE AI / Agnes AI"
|
|
90
|
+
if route in {"music", "musica", "música"} or bool(task.get("music_mode")):
|
|
91
|
+
return "Suno / Ficheiro local"
|
|
92
|
+
stage = str(task.get("stage") or task.get("failed_stage") or "").strip().casefold()
|
|
93
|
+
if stage in {"topic", "script", "title", "keywords", "thumbnail_prompt"}:
|
|
94
|
+
return "LLM textual API (provider não registado)"
|
|
95
|
+
if stage == "thumbnail":
|
|
96
|
+
return "Pool de imagem API (provider não registado)"
|
|
97
|
+
if stage == "upload":
|
|
98
|
+
return "YouTube Upload API"
|
|
99
|
+
return "API não identificada (falha anterior)"
|
|
100
|
+
|
|
101
|
+
|
|
79
102
|
def _task_log(task: dict[str, Any]) -> dict[str, Any] | None:
|
|
80
103
|
task_id = str(task.get("id") or "").strip()
|
|
81
104
|
if not task_id:
|
|
@@ -100,6 +123,20 @@ def _task_log(task: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
100
123
|
if progress_value is not None:
|
|
101
124
|
details.append(f"Progresso: {progress_value}%")
|
|
102
125
|
error = str(task.get("error") or "").strip()
|
|
126
|
+
failure_api = str(task.get("failure_api") or "").strip()
|
|
127
|
+
failure_provider = str(task.get("failure_provider") or "").strip()
|
|
128
|
+
failure_service = str(task.get("failure_service") or "").strip()
|
|
129
|
+
failure_fields = str(task.get("failure_config_fields") or "").strip()
|
|
130
|
+
api_provider = failure_api
|
|
131
|
+
if state in {"failed", "error"}:
|
|
132
|
+
api_provider = api_provider or _fallback_task_failure_api(task)
|
|
133
|
+
details.append(f"API/provider: {api_provider}")
|
|
134
|
+
if failure_provider:
|
|
135
|
+
details.append(f"Provider: {failure_provider}")
|
|
136
|
+
if failure_service:
|
|
137
|
+
details.append(f"Serviço: {failure_service}")
|
|
138
|
+
if failure_fields:
|
|
139
|
+
details.append(f"Configuração: {failure_fields}")
|
|
103
140
|
if error:
|
|
104
141
|
details.append(error[:500])
|
|
105
142
|
return {
|
|
@@ -114,6 +151,7 @@ def _task_log(task: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
114
151
|
"source": "Tarefas",
|
|
115
152
|
"record": title,
|
|
116
153
|
"details": " · ".join(details),
|
|
154
|
+
"api_provider": api_provider or "",
|
|
117
155
|
"progress": progress_value,
|
|
118
156
|
"task_id": task_id,
|
|
119
157
|
}
|
|
@@ -141,6 +179,9 @@ def _notification_log(entry: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
141
179
|
date, time = format_log_date_time(occurred_at)
|
|
142
180
|
metadata = entry.get("metadata") if isinstance(entry.get("metadata"), dict) else {}
|
|
143
181
|
public_metadata = [f"{key}: {value}" for key, value in metadata.items() if value not in (None, "")]
|
|
182
|
+
api_provider = str(metadata.get("failure_api") or metadata.get("api_provider") or "").strip()
|
|
183
|
+
if status_code in {"failed", "error"}:
|
|
184
|
+
api_provider = api_provider or "API não identificada (falha anterior)"
|
|
144
185
|
return {
|
|
145
186
|
"id": f"notification:{entry_id}",
|
|
146
187
|
"operation_code": event_type,
|
|
@@ -153,6 +194,7 @@ def _notification_log(entry: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
153
194
|
"source": "Notificações",
|
|
154
195
|
"record": str(entry.get("title") or entry.get("label") or "Notificação"),
|
|
155
196
|
"details": " · ".join([str(entry.get("message") or "").strip(), *public_metadata]).strip(" ·")[:1000],
|
|
197
|
+
"api_provider": api_provider,
|
|
156
198
|
"progress": None,
|
|
157
199
|
"task_id": str(metadata.get("task_id") or ""),
|
|
158
200
|
}
|
|
@@ -227,6 +269,7 @@ def logs_to_rows(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
|
227
269
|
"Registo": item.get("record") or "—",
|
|
228
270
|
"Origem": item.get("source") or "—",
|
|
229
271
|
"Progresso": f"{item['progress']}%" if item.get("progress") is not None else "—",
|
|
272
|
+
"API/Provider": item.get("api_provider") or "—",
|
|
230
273
|
"Detalhes": item.get("details") or "—",
|
|
231
274
|
}
|
|
232
275
|
for item in records
|
|
@@ -489,9 +489,13 @@ def generate_video_from_pool(
|
|
|
489
489
|
*,
|
|
490
490
|
image_url: str = "",
|
|
491
491
|
output_path: Path | None = None,
|
|
492
|
+
allowed_providers: set[str] | None = None,
|
|
492
493
|
) -> Path:
|
|
493
|
-
"""Try eligible video cards in priority order,
|
|
494
|
+
"""Try eligible video cards in priority order, optionally restricted to a route."""
|
|
494
495
|
cards = media_cards_for_pool(settings, "video")
|
|
496
|
+
if allowed_providers is not None:
|
|
497
|
+
allowed = {str(provider).strip().lower() for provider in allowed_providers}
|
|
498
|
+
cards = [card for card in cards if str(card.get("provider") or "").strip().lower() in allowed]
|
|
495
499
|
if not cards:
|
|
496
500
|
raise MediaGenerationError("Não existem providers activos no pool de vídeo.")
|
|
497
501
|
errors: list[str] = []
|
|
@@ -236,7 +236,24 @@ def _task_notifications() -> int:
|
|
|
236
236
|
title = str(task.get("title") or task.get("topic") or "Actividade")
|
|
237
237
|
channel = str(task.get("channel_name") or "Canal")
|
|
238
238
|
if state == "failed":
|
|
239
|
-
|
|
239
|
+
failure_api = str(task.get("failure_api") or "API não identificada (falha anterior)").strip()
|
|
240
|
+
if record_notification(
|
|
241
|
+
"activity_failed",
|
|
242
|
+
f"Actividade falhou: {title}",
|
|
243
|
+
f"A actividade de {channel} terminou com erro. API/provider: {failure_api}.",
|
|
244
|
+
metadata={
|
|
245
|
+
"task_id": task_id,
|
|
246
|
+
"channel_name": channel,
|
|
247
|
+
"error": task.get("error") or "",
|
|
248
|
+
"failure_api": task.get("failure_api") or "API não identificada (falha anterior)",
|
|
249
|
+
"failure_provider": task.get("failure_provider") or "unknown",
|
|
250
|
+
"failure_service": task.get("failure_service") or "Thunderbolt",
|
|
251
|
+
"failure_route": task.get("failure_route") or "",
|
|
252
|
+
"failure_config_fields": task.get("failure_config_fields") or "",
|
|
253
|
+
"failure_stage": task.get("failure_stage") or task.get("failed_stage") or task.get("stage") or "pipeline",
|
|
254
|
+
},
|
|
255
|
+
dedupe_key=f"task:{task_id}:failed",
|
|
256
|
+
):
|
|
240
257
|
created += 1
|
|
241
258
|
continue
|
|
242
259
|
if state != "done":
|