@danhachuel/thunderbolt 0.3.60 → 0.3.62
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 +9 -3
- package/app/main.py +24 -5
- package/hermes_ui/automation_worker.py +6 -1
- package/hermes_ui/domain.py +1 -0
- package/hermes_ui/influencers.py +12 -6
- package/hermes_ui/media_generation.py +5 -1
- package/hermes_ui/pipeline_worker.py +220 -13
- package/package.json +1 -3
- package/scripts/cli.mjs +4 -4
- package/scripts/install.mjs +53 -1
- 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.62
|
|
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)
|
|
@@ -33,7 +33,7 @@ A área **AI Influencers > Personagens** permite criar personagens com nome, bio
|
|
|
33
33
|
|
|
34
34
|
O backend local predefinido é **SQLite**, pelo que Personagens e Geração de Conteúdo IA podem ser usados sem credenciais externas. Abra **Configurações > Configuração API > AI Influencers > Banco de Dados Influencers** para confirmar **SQLite** ou seleccionar **Supabase**. Se seleccionar Supabase, preencha **Supabase Project URL**, **Supabase API key** e **Supabase Storage bucket**, clique em **Testar ligação do backend**, aplique primeiro `seed/references/ai_influencers_schema.sql` no SQL Editor do projecto, exponha as quatro tabelas na Data API e configure permissões/RLS adequadas. O bucket deve existir com o nome configurado, por defeito `ai-influencers`.
|
|
35
35
|
|
|
36
|
-
Se pretender trabalhar sem serviço externo, seleccione **SQLite** no selector da aba **Configuração API > AI Influencers** e indique o caminho do ficheiro. O padrão é `storage/state/ai_influencers.db`; o SQLite cria o schema e guarda os assets em `storage/influencers/`. Apenas um backend é usado de cada vez. A aba **AI Influencers** mostra o estado e permite editar directamente o selector, os campos Supabase e o caminho SQLite.
|
|
36
|
+
Se pretender trabalhar sem serviço externo, seleccione **SQLite** no selector da aba **Configuração API > AI Influencers** e indique o caminho do ficheiro. O padrão é `storage/state/ai_influencers.db`; o SQLite cria o schema e guarda os assets em `storage/influencers/`. Apenas um backend é usado de cada vez. A aba **AI Influencers** mostra o estado e permite editar directamente o selector, os campos Supabase e o caminho SQLite. A base fica na pasta persistente do Thunderbolt, fora da instalação temporária do pacote npm; numa actualização normal, personagens, assets e configurações são preservados. O instalador também procura uma base `ai_influencers.db` válida em instalações anteriores do cache npm e recupera-a sem substituir uma base persistente já existente.
|
|
37
37
|
|
|
38
38
|
Em **AI Influencers > Geração de Conteúdo IA**, utilize as subabas **Imagens**, **Vídeos** e **Motion Control**. Seleccione um personagem, prompt e destinos sociais. A subaba **Imagens** usa o pool de imagem configurado em **Imagem e Video**. A subaba **Vídeos** requer uma imagem inicial e usa o pool de vídeo; escolha o provider/modelo no cartão configurado, sem dependência obrigatória do Veo 3.1. KIE AI, Replicate e FAL AI podem usar tarefas assíncronas; o Thunderbolt consulta o estado e guarda o resultado local antes da revisão. Na Replicate, o campo Modelo deve ser o identificador do modelo ou da versão, e os inputs específicos dependem do modelo configurado.
|
|
39
39
|
|
|
@@ -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. **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.62 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.62 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.62 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.
|
|
@@ -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.62 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
|
|
|
@@ -32,12 +32,18 @@ A primeira versão implementa a camada UI independente com:
|
|
|
32
32
|
|
|
33
33
|
A área **AI Influencers > Personagens** permite guardar um personagem com nome, biografia, idioma, Instagram Business ID opcional, várias imagens de referência e documentos `.md` ou `.json`. O campo **Idioma** é um selector que reutiliza exactamente a lista, os códigos e os labels visuais de **Pipeline Vídeos > Criação de Vídeos**. Cada asset é validado, sanitizado, deduplicado por SHA-256 e guardado separado do perfil; imagens têm pré-visualização e documentos são mostrados como Markdown ou JSON estruturado.
|
|
34
34
|
|
|
35
|
-
O backend local predefinido é **SQLite**, por isso Personagens e Geração de Conteúdo IA funcionam imediatamente sem credenciais externas. Em **Configurações > Configuração API > AI Influencers > Banco de Dados Influencers**, pode manter **SQLite** ou seleccionar **Supabase** quando quiser uma base remota; nesse caso, configure o **Supabase Project URL**, a **Supabase API key** e o bucket de Storage, aplique `seed/references/ai_influencers_schema.sql` no SQL Editor e confirme Data API/RLS. SQLite cria localmente `storage/state/ai_influencers.db` e `storage/influencers/`; apenas um backend fica activo de cada vez. A aba **AI Influencers** mostra o estado actual e permite editar directamente o selector, as credenciais Supabase, o bucket de Storage e o caminho do SQLite. O botão **Testar ligação do backend** é read-only e não bloqueia a utilização local.
|
|
35
|
+
O backend local predefinido é **SQLite**, por isso Personagens e Geração de Conteúdo IA funcionam imediatamente sem credenciais externas. Em **Configurações > Configuração API > AI Influencers > Banco de Dados Influencers**, pode manter **SQLite** ou seleccionar **Supabase** quando quiser uma base remota; nesse caso, configure o **Supabase Project URL**, a **Supabase API key** e o bucket de Storage, aplique `seed/references/ai_influencers_schema.sql` no SQL Editor e confirme Data API/RLS. SQLite cria localmente `storage/state/ai_influencers.db` e `storage/influencers/`; apenas um backend fica activo de cada vez. A aba **AI Influencers** mostra o estado actual e permite editar directamente o selector, as credenciais Supabase, o bucket de Storage e o caminho do SQLite. O botão **Testar ligação do backend** é read-only e não bloqueia a utilização local. A base SQLite fica na pasta persistente do Thunderbolt, fora da instalação temporária do pacote npm; durante uma actualização normal, personagens, assets e configurações são preservados. Se uma versão anterior tiver guardado `ai_influencers.db` no cache npm, o instalador procura uma cópia válida e recupera-a sem substituir uma base persistente já existente.
|
|
36
36
|
|
|
37
37
|
A página **AI Influencers > Geração de Conteúdo IA** contém as subabas **Imagens**, **Vídeos** e **Motion Control**. Imagens usam os cartões activos do **Pool Imagem**; Vídeos usam os cartões activos do **Pool Vídeo** e exigem uma imagem inicial image-to-video. O selector de modelo não está preso ao Veo 3.1: pode utilizar KIE AI, Replicate, FAL AI, Pollinations ou outro cartão que declare suporte para vídeo. Para Replicate, o campo Modelo deve conter o identificador aceito pela API, como `owner/model` ou `owner/model:version`; a tarefa usa `POST /v1/predictions`, consulta o estado assíncrono e guarda o resultado localmente. A publicação para Instagram, TikTok, YouTube Shorts ou Facebook não é automática: os destinos são registados para revisão e o envio exige uma acção posterior explícita.
|
|
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
|
+
|
|
41
47
|
## Upload-Post — publicação para múltiplas plataformas
|
|
42
48
|
|
|
43
49
|
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 +85,7 @@ No topo da área principal da aplicação existe o menu nativo de idioma no padr
|
|
|
79
85
|
|
|
80
86
|
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
87
|
|
|
82
|
-
## Navegação da UI 0.3.
|
|
88
|
+
## Navegação da UI 0.3.62
|
|
83
89
|
|
|
84
90
|
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
91
|
**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
|
|
|
@@ -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", "")),
|
package/hermes_ui/influencers.py
CHANGED
|
@@ -211,14 +211,21 @@ class InfluencerBackendError(RuntimeError):
|
|
|
211
211
|
"""Raised when the selected database backend is unavailable or misconfigured."""
|
|
212
212
|
|
|
213
213
|
|
|
214
|
+
def _resolve_sqlite_path(path: str | Path | None = None) -> Path:
|
|
215
|
+
raw_path = Path(path or (STORAGE / "state" / "ai_influencers.db")).expanduser()
|
|
216
|
+
if raw_path.is_absolute():
|
|
217
|
+
return raw_path.resolve()
|
|
218
|
+
parts = raw_path.parts
|
|
219
|
+
if parts and parts[0].casefold() == "storage":
|
|
220
|
+
raw_path = Path(*parts[1:]) if len(parts) > 1 else Path("state") / "ai_influencers.db"
|
|
221
|
+
return (STORAGE / raw_path).resolve()
|
|
222
|
+
|
|
223
|
+
|
|
214
224
|
class SQLiteInfluencerRepository:
|
|
215
225
|
backend = "SQLite"
|
|
216
226
|
|
|
217
227
|
def __init__(self, path: str | Path | None = None):
|
|
218
|
-
|
|
219
|
-
if not candidate.is_absolute():
|
|
220
|
-
candidate = ROOT / candidate
|
|
221
|
-
self.path = candidate.resolve()
|
|
228
|
+
self.path = _resolve_sqlite_path(path)
|
|
222
229
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
223
230
|
|
|
224
231
|
def ensure_schema(self) -> None:
|
|
@@ -511,8 +518,7 @@ class SupabaseInfluencerRepository:
|
|
|
511
518
|
|
|
512
519
|
def sqlite_path_from_settings(settings: Mapping[str, Any]) -> Path:
|
|
513
520
|
raw = str(settings.get("influencer_sqlite_path") or "storage/state/ai_influencers.db").strip()
|
|
514
|
-
|
|
515
|
-
return path if path.is_absolute() else ROOT / path
|
|
521
|
+
return _resolve_sqlite_path(raw)
|
|
516
522
|
|
|
517
523
|
|
|
518
524
|
def backend_name(settings: Mapping[str, Any]) -> str:
|
|
@@ -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] = []
|
|
@@ -11,6 +11,7 @@ from datetime import datetime, timezone
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
+
from integrations.moneyprinter_config import sync_moneyprinter_config
|
|
14
15
|
from integrations.upload_routing import upload_with_default_route
|
|
15
16
|
from hermes_ui.creative_generation import CreativeGenerationError, generate_creative_package, generate_title_and_keywords, generate_thumbnail_prompt, generate_topic_for_channel
|
|
16
17
|
from hermes_ui.script_documents import save_script_document
|
|
@@ -19,6 +20,7 @@ from hermes_ui.storage import STORAGE, ensure_storage, read_json, write_json
|
|
|
19
20
|
from hermes_ui.llm_providers import active_llm_card, provider_definition
|
|
20
21
|
from hermes_ui.media_generation import MediaGenerationError, _append_generation_constraints, generate_image_from_pool, generate_video_from_pool
|
|
21
22
|
from hermes_ui.media_providers import media_cards_for_pool
|
|
23
|
+
from hermes_ui.material_sources import material_api_keys, selected_material_source
|
|
22
24
|
from hermes_ui.thumbnail_generation import ThumbnailGenerationError, generate_thumbnail_image
|
|
23
25
|
|
|
24
26
|
PIPELINE_LOCK_FILENAME = "pipeline_worker.lock"
|
|
@@ -324,6 +326,156 @@ def _stop_process(process: subprocess.Popen[str]) -> None:
|
|
|
324
326
|
process.wait()
|
|
325
327
|
|
|
326
328
|
|
|
329
|
+
def _normalise_video_route(task: dict[str, Any], settings: dict[str, Any]) -> str:
|
|
330
|
+
"""Resolve the per-task source without conflating stock, AI and music routes."""
|
|
331
|
+
generation_settings = task.get("generation_settings") if isinstance(task.get("generation_settings"), dict) else {}
|
|
332
|
+
if bool(task.get("music_mode")):
|
|
333
|
+
return "music"
|
|
334
|
+
explicit_source = str(
|
|
335
|
+
task.get("material_source")
|
|
336
|
+
or generation_settings.get("material_source")
|
|
337
|
+
or generation_settings.get("video_material_source")
|
|
338
|
+
or ""
|
|
339
|
+
).strip().casefold()
|
|
340
|
+
if explicit_source in {"pexels", "pixabay", "local"}:
|
|
341
|
+
return explicit_source
|
|
342
|
+
raw = str(task.get("style_wide") or generation_settings.get("video_source") or "pexels").strip().casefold()
|
|
343
|
+
if raw in {"full_ia", "full ia", "full-ai", "ai", "ia"}:
|
|
344
|
+
return "full_ia"
|
|
345
|
+
if raw in {"music", "apenas música", "apenas musica", "only music"}:
|
|
346
|
+
return "music"
|
|
347
|
+
if raw in {"pixabay", "pixabay only"}:
|
|
348
|
+
return "pixabay"
|
|
349
|
+
if raw in {"pexels", "pexels/pixabay", "stock", "materials", "materiales"}:
|
|
350
|
+
configured = selected_material_source(settings)
|
|
351
|
+
return configured if configured in {"pexels", "pixabay"} else "pexels"
|
|
352
|
+
return raw if raw in {"pexels", "pixabay", "local"} else "pexels"
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _mpt_aspect_ratio(value: Any, format_value: Any = "") -> str:
|
|
356
|
+
raw = str(value or format_value or "").strip().casefold()
|
|
357
|
+
if raw in {"landscape 16:9", "wide", "16:9", "landscape"}:
|
|
358
|
+
return "16:9"
|
|
359
|
+
if raw in {"square 1:1", "square", "1:1"}:
|
|
360
|
+
return "1:1"
|
|
361
|
+
return "9:16"
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _mpt_concat_mode(value: Any) -> str:
|
|
365
|
+
raw = str(value or "").strip().casefold()
|
|
366
|
+
return "sequential" if raw in {"sequential", "sequential concatenation", "ordem sequencial"} else "random"
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _mpt_transition_mode(value: Any) -> str:
|
|
370
|
+
raw = str(value or "").strip().casefold()
|
|
371
|
+
return {"fade": "fade-in", "dissolve": "fade-out", "fade-in": "fade-in", "fade-out": "fade-out"}.get(raw, "none")
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _mpt_percent(value: Any, fallback: float = 1.0) -> str:
|
|
375
|
+
raw = str(value or "").strip().replace("%", "")
|
|
376
|
+
try:
|
|
377
|
+
return str(max(0.0, min(2.0, float(raw) / 100.0)))
|
|
378
|
+
except ValueError:
|
|
379
|
+
return str(fallback)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _valid_audio_artifact(value: Any) -> Path | None:
|
|
383
|
+
path = _valid_artifact_path(value)
|
|
384
|
+
if path is None or path.suffix.lower() not in {".mp3", ".wav", ".m4a", ".aac", ".flac", ".ogg"}:
|
|
385
|
+
return None
|
|
386
|
+
return path
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _mpt_rate(value: Any, fallback: float = 1.0) -> str:
|
|
390
|
+
raw = str(value or "").strip().casefold().replace("x", "")
|
|
391
|
+
try:
|
|
392
|
+
return str(max(0.1, float(raw)))
|
|
393
|
+
except ValueError:
|
|
394
|
+
return str(fallback)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _mpt_video_language(value: Any) -> str:
|
|
398
|
+
"""Convert common Thunderbolt labels to the language codes accepted by MPT."""
|
|
399
|
+
raw = str(value or "").strip()
|
|
400
|
+
folded = raw.casefold()
|
|
401
|
+
aliases = {
|
|
402
|
+
"português": "pt-BR", "portugues": "pt-BR", "português (brasil)": "pt-BR",
|
|
403
|
+
"portuguese": "pt-BR", "inglês": "en-US", "ingles": "en-US", "english": "en-US",
|
|
404
|
+
"espanhol": "es-ES", "español": "es-ES", "francês": "fr-FR", "frances": "fr-FR",
|
|
405
|
+
"alemão": "de-DE", "alemao": "de-DE", "italiano": "it-IT", "japonês": "ja-JP",
|
|
406
|
+
"japones": "ja-JP", "mandarim": "zh-CN", "chinês": "zh-CN", "chines": "zh-CN",
|
|
407
|
+
}
|
|
408
|
+
if folded in aliases:
|
|
409
|
+
return aliases[folded]
|
|
410
|
+
return raw if re.fullmatch(r"[A-Za-z]{2,3}(?:-[A-Za-z]{2,4})?", raw) else ""
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _moneyprinter_cli_args(task: dict[str, Any], route: str) -> list[str]:
|
|
414
|
+
"""Build the explicit MPT CLI contract for the stock Pexels/Pixabay route."""
|
|
415
|
+
generation_settings = task.get("generation_settings") if isinstance(task.get("generation_settings"), dict) else {}
|
|
416
|
+
args: list[str] = ["--video-source", route]
|
|
417
|
+
script = str(task.get("video_script") or generation_settings.get("video_script") or "").strip()
|
|
418
|
+
if script:
|
|
419
|
+
args.extend(["--video-script", script])
|
|
420
|
+
keywords = task.get("video_keywords") or generation_settings.get("video_keywords") or task.get("keywords") or task.get("tags")
|
|
421
|
+
if isinstance(keywords, str):
|
|
422
|
+
keywords = [part.strip() for part in re.split(r"[,;|\n]+", keywords) if part.strip()]
|
|
423
|
+
if isinstance(keywords, list):
|
|
424
|
+
terms = [str(item).strip() for item in keywords if str(item).strip()]
|
|
425
|
+
if terms:
|
|
426
|
+
args.extend(["--video-terms", ",".join(terms[:15])])
|
|
427
|
+
|
|
428
|
+
language = _mpt_video_language(task.get("language"))
|
|
429
|
+
if language:
|
|
430
|
+
args.extend(["--video-language", language])
|
|
431
|
+
format_value = str(task.get("format") or generation_settings.get("video_format") or "").strip().casefold()
|
|
432
|
+
if format_value in {"wide", "landscape", "16:9"}:
|
|
433
|
+
aspect_ratio = "16:9"
|
|
434
|
+
elif format_value in {"shorts", "portrait", "9:16"}:
|
|
435
|
+
aspect_ratio = "9:16"
|
|
436
|
+
else:
|
|
437
|
+
aspect_ratio = _mpt_aspect_ratio(generation_settings.get("video_aspect_ratio"), task.get("format"))
|
|
438
|
+
args.extend(["--video-aspect", aspect_ratio])
|
|
439
|
+
args.extend(["--video-concat-mode", _mpt_concat_mode(generation_settings.get("video_concatenation_mode"))])
|
|
440
|
+
args.extend(["--video-transition-mode", _mpt_transition_mode(generation_settings.get("video_transition_mode"))])
|
|
441
|
+
clip_duration = generation_settings.get("maximum_clip_duration")
|
|
442
|
+
if str(clip_duration or "").strip().isdigit() and int(clip_duration) > 0:
|
|
443
|
+
args.extend(["--video-clip-duration", str(int(clip_duration))])
|
|
444
|
+
if bool(generation_settings.get("match_visuals_to_script_order")):
|
|
445
|
+
args.append("--match-materials-to-script")
|
|
446
|
+
|
|
447
|
+
voice_mode = str(generation_settings.get("voiceover_mode") or "").strip().casefold()
|
|
448
|
+
voice = str(task.get("voice") or generation_settings.get("voice") or "").strip()
|
|
449
|
+
if voice_mode == "none" or voice_mode == "upload":
|
|
450
|
+
args.extend(["--voice-name", "no-voice"])
|
|
451
|
+
elif voice:
|
|
452
|
+
args.extend(["--voice-name", voice])
|
|
453
|
+
volume = generation_settings.get("voiceover_volume")
|
|
454
|
+
speed = generation_settings.get("voiceover_speed")
|
|
455
|
+
if volume is not None:
|
|
456
|
+
args.extend(["--voice-volume", _mpt_percent(volume)])
|
|
457
|
+
if speed is not None:
|
|
458
|
+
args.extend(["--voice-rate", _mpt_rate(speed)])
|
|
459
|
+
|
|
460
|
+
subtitles = generation_settings.get("enable_subtitles")
|
|
461
|
+
if subtitles is not None:
|
|
462
|
+
args.append("--subtitle-enabled" if bool(subtitles) else "--no-subtitle-enabled")
|
|
463
|
+
subtitle_position = str(generation_settings.get("subtitle_position") or "").strip().casefold()
|
|
464
|
+
if subtitle_position in {"top", "center", "bottom", "custom"}:
|
|
465
|
+
args.extend(["--subtitle-position", subtitle_position])
|
|
466
|
+
font_name = str(generation_settings.get("subtitle_font") or "").strip()
|
|
467
|
+
if font_name:
|
|
468
|
+
args.extend(["--font-name", font_name])
|
|
469
|
+
bgm_source = str(generation_settings.get("background_music_source") or "").strip().casefold()
|
|
470
|
+
bgm_type = {"sem música": "none", "sem musica": "none", "random background music": "random", "ficheiro existente": "custom"}.get(bgm_source)
|
|
471
|
+
if bgm_type:
|
|
472
|
+
args.extend(["--bgm-type", bgm_type])
|
|
473
|
+
bgm_volume = generation_settings.get("background_music_volume")
|
|
474
|
+
if bgm_volume is not None:
|
|
475
|
+
args.extend(["--bgm-volume", _mpt_percent(bgm_volume)])
|
|
476
|
+
return args
|
|
477
|
+
|
|
478
|
+
|
|
327
479
|
def _run_video_helper(task: dict[str, Any]) -> Path:
|
|
328
480
|
helper_dir = Path(__file__).resolve().parents[1] / "seed" / "skills"
|
|
329
481
|
helper = helper_dir / "mpt_agent.py"
|
|
@@ -341,12 +493,27 @@ def _run_video_helper(task: dict[str, Any]) -> Path:
|
|
|
341
493
|
card = active_llm_card(settings)
|
|
342
494
|
provider = str(card.get("provider") or "openai").strip()
|
|
343
495
|
definition = provider_definition(provider)
|
|
496
|
+
route = _normalise_video_route(task, settings)
|
|
497
|
+
source_keys = material_api_keys(settings, route) if route in {"pexels", "pixabay"} else []
|
|
498
|
+
if route in {"pexels", "pixabay"} and not source_keys:
|
|
499
|
+
source_label = "Pexels" if route == "pexels" else "Pixabay"
|
|
500
|
+
raise PipelineError(
|
|
501
|
+
f"Configure pelo menos uma API key de {source_label} em Configurações > Configuração API > Fontes de materiais."
|
|
502
|
+
)
|
|
503
|
+
if configured_root:
|
|
504
|
+
try:
|
|
505
|
+
sync_moneyprinter_config(settings, str(configured_root))
|
|
506
|
+
except OSError as exc:
|
|
507
|
+
raise PipelineError(f"Não foi possível sincronizar a configuração do MoneyPrinterTurbo: {exc}") from exc
|
|
344
508
|
env_values = {
|
|
345
509
|
"MPT_LLM_PROVIDER": provider,
|
|
346
510
|
"MPT_LLM_API_KEY": str(card.get("api_key") or "").strip(),
|
|
347
511
|
"MPT_LLM_BASE_URL": str(card.get("base_url") or definition.default_base_url or "").strip(),
|
|
348
512
|
"MPT_LLM_MODEL_NAME": str(card.get("model") or "").strip(),
|
|
349
|
-
"MPT_PEXELS_API_KEY":
|
|
513
|
+
"MPT_PEXELS_API_KEY": source_keys[0] if route == "pexels" and source_keys else "",
|
|
514
|
+
"MPT_PEXELS_API_KEYS": json.dumps(source_keys, ensure_ascii=False) if route == "pexels" and source_keys else "",
|
|
515
|
+
"MPT_PIXABAY_API_KEY": source_keys[0] if route == "pixabay" and source_keys else "",
|
|
516
|
+
"MPT_PIXABAY_API_KEYS": json.dumps(source_keys, ensure_ascii=False) if route == "pixabay" and source_keys else "",
|
|
350
517
|
}
|
|
351
518
|
for key, value in env_values.items():
|
|
352
519
|
if value:
|
|
@@ -356,6 +523,7 @@ def _run_video_helper(task: dict[str, Any]) -> Path:
|
|
|
356
523
|
command.extend(["--root", str(configured_root)])
|
|
357
524
|
command.extend(["--subject", subject])
|
|
358
525
|
generation_settings = task.get("generation_settings") if isinstance(task.get("generation_settings"), dict) else {}
|
|
526
|
+
command.extend(_moneyprinter_cli_args(task, route))
|
|
359
527
|
if str(generation_settings.get("voiceover_mode") or "").strip().casefold() == "upload":
|
|
360
528
|
voiceover_file = Path(str(generation_settings.get("voiceover_file") or "").strip()).expanduser()
|
|
361
529
|
if not str(voiceover_file) or not voiceover_file.is_file() or voiceover_file.stat().st_size <= 0:
|
|
@@ -516,8 +684,9 @@ def _run_task(task: dict[str, Any]) -> dict[str, Any]:
|
|
|
516
684
|
channel = _channel_for_task(task)
|
|
517
685
|
settings = _settings()
|
|
518
686
|
blueprint = _blueprint_for_channel(channel)
|
|
687
|
+
route = _normalise_video_route(task, settings)
|
|
519
688
|
topic = str(task.get("topic") or "").strip()
|
|
520
|
-
if not topic or str(task.get("topic_source") or "") in {"auto", "llm_pending"}:
|
|
689
|
+
if route != "music" and (not topic or str(task.get("topic_source") or "") in {"auto", "llm_pending"}):
|
|
521
690
|
_update(task_id, stage="topic", state="doing", progress=5, error=None)
|
|
522
691
|
topic_result = generate_topic_for_channel(settings, channel, blueprint, user_context=str(task.get("topic_context") or ""))
|
|
523
692
|
topic = str(topic_result.get("topic") or "").strip()
|
|
@@ -527,6 +696,32 @@ def _run_task(task: dict[str, Any]) -> dict[str, Any]:
|
|
|
527
696
|
|
|
528
697
|
generation_settings = task.get("generation_settings") if isinstance(task.get("generation_settings"), dict) else {}
|
|
529
698
|
artifacts = dict(task.get("artifacts") or {})
|
|
699
|
+
if route == "music":
|
|
700
|
+
# Apenas Música não é uma tarefa de vídeo: reutiliza o áudio preparado e
|
|
701
|
+
# termina pronta para a integração de upload musical, sem chamar LLM,
|
|
702
|
+
# Pexels/Pixabay, MoviePy de vídeo, thumbnail ou upload de vídeo.
|
|
703
|
+
music_candidates = [
|
|
704
|
+
task.get("music_path"),
|
|
705
|
+
generation_settings.get("music_path"),
|
|
706
|
+
artifacts.get("music"),
|
|
707
|
+
]
|
|
708
|
+
music_path = next((path for path in (_valid_audio_artifact(item) for item in music_candidates) if path is not None), None)
|
|
709
|
+
if music_path is None:
|
|
710
|
+
raise PipelineError("A fonte Apenas Música exige um ficheiro de áudio local válido ou uma música Suno já descarregada antes de criar a tarefa.")
|
|
711
|
+
artifacts["music"] = str(music_path)
|
|
712
|
+
_update(
|
|
713
|
+
task_id,
|
|
714
|
+
stage="upload",
|
|
715
|
+
state="done",
|
|
716
|
+
progress=100,
|
|
717
|
+
artifacts=artifacts,
|
|
718
|
+
music_ready=True,
|
|
719
|
+
video_ready=False,
|
|
720
|
+
thumbnail_status="not_applicable",
|
|
721
|
+
error=None,
|
|
722
|
+
)
|
|
723
|
+
return _task_by_id(task_id) or task
|
|
724
|
+
|
|
530
725
|
script = _read_persisted_script(task, channel, blueprint, topic)
|
|
531
726
|
if script is None:
|
|
532
727
|
_update(task_id, stage="script", state="doing", progress=18, error=None)
|
|
@@ -618,18 +813,30 @@ def _run_task(task: dict[str, Any]) -> dict[str, Any]:
|
|
|
618
813
|
existing_video = _valid_artifact_path(artifacts.get("video"))
|
|
619
814
|
if existing_video is None:
|
|
620
815
|
_update(task_id, stage="video", state="doing", progress=max(52, int(task.get("progress") or 0)), error=None)
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
816
|
+
try:
|
|
817
|
+
video_prompt = _append_generation_constraints(
|
|
818
|
+
f"Título: {title}\n\nRoteiro:\n{str(script.get('content') or '')[:12000]}",
|
|
819
|
+
kind="video",
|
|
820
|
+
)
|
|
821
|
+
if route == "full_ia":
|
|
822
|
+
video_path = generate_video_from_pool(
|
|
823
|
+
settings,
|
|
824
|
+
video_prompt,
|
|
825
|
+
allowed_providers={"fal_ai", "kie_ai", "agnes"},
|
|
627
826
|
)
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
827
|
+
else:
|
|
828
|
+
video_path = _run_video_helper({
|
|
829
|
+
**task,
|
|
830
|
+
"topic": topic,
|
|
831
|
+
"title": title,
|
|
832
|
+
"video_script": str(script.get("content") or ""),
|
|
833
|
+
"video_keywords": keywords,
|
|
834
|
+
"style_wide": route,
|
|
835
|
+
})
|
|
836
|
+
except MediaGenerationError as exc:
|
|
837
|
+
if route == "full_ia":
|
|
838
|
+
raise PipelineError(f"Pool Full IA (FAL/KIE AI/Agnes AI): {exc}") from exc
|
|
839
|
+
raise PipelineError(f"Pipeline MoneyPrinterTurbo ({route}): {exc}") from exc
|
|
633
840
|
current_after_video = _task_by_id(task_id) or {}
|
|
634
841
|
artifacts = dict(current_after_video.get("artifacts") or artifacts)
|
|
635
842
|
artifacts["video"] = str(video_path)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danhachuel/thunderbolt",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.62",
|
|
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",
|
|
@@ -16,8 +16,6 @@
|
|
|
16
16
|
"integrations/*.py",
|
|
17
17
|
"integrations/data/*.json",
|
|
18
18
|
"scripts/*.mjs",
|
|
19
|
-
"storage/blueprints/**/*.json",
|
|
20
|
-
"storage/tiktok/prompts_master/**/*.md",
|
|
21
19
|
"seed/prompt_masters/**/*.md",
|
|
22
20
|
"seed/blueprints/**/*.json",
|
|
23
21
|
"seed/skills/*.md",
|