@danhachuel/thunderbolt 0.2.75 → 0.2.76
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 +19 -11
- package/README.md +17 -13
- package/app/main.py +226 -210
- package/hermes_ui/material_sources.py +64 -0
- package/hermes_ui/storage.py +3 -0
- package/integrations/moneyprinter_config.py +9 -3
- package/package.json +1 -1
package/MANUAL-INSTALACAO.md
CHANGED
|
@@ -100,13 +100,13 @@ Execute:
|
|
|
100
100
|
Windows PowerShell ou MobaXterm:
|
|
101
101
|
|
|
102
102
|
```powershell
|
|
103
|
-
npx.cmd --yes @danhachuel/thunderbolt@0.2.
|
|
103
|
+
npx.cmd --yes @danhachuel/thunderbolt@0.2.76 install
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
Linux/macOS:
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
|
-
npx --yes @danhachuel/thunderbolt@0.2.
|
|
109
|
+
npx --yes @danhachuel/thunderbolt@0.2.76 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.
|
|
@@ -388,14 +388,14 @@ Após iniciar a aplicação, valide o seguinte percurso:
|
|
|
388
388
|
3. **Brandings:** abra a subaba **Brandings** e confirme a listagem dos ficheiros JSON.
|
|
389
389
|
4. **Canais:** em **Importar do YouTube**, use o método **Página pública — sem API Key** com um URL `/channel/UC...`, um handle ou uma subpágina `/videos`; o parser resolve o ID, consulta a página pública e tenta o RSS quando necessário. Confirme que o resultado abre o formulário de revisão sem Data API Key. Se o canal não existir ou não fornecer metadados, confirme a mensagem clara e que o formulário de uma pesquisa anterior desaparece. A Data API é opcional e fica separada; em **Cadastro manual**, preencha os dados sem qualquer consulta externa.
|
|
390
390
|
5. **Niche Finder Kaggle:** abra o menu expansível **Niche Finder**, seleccione **Niche Finder Kaggle**, defina os parâmetros dentro do conteúdo principal e confirme que não há preparação automática. Antes do clique, não deve existir download de dataset nem análise. Clique em **Analisar Nichos** para iniciar a preparação dos dados e a análise; depois altere país, engagement, datas e tags e clique novamente para aplicar os novos parâmetros. Em seguida, abra a alternativa independente **Niche Finder Apify**, configure as palavras-chave e filtros na própria aba e confirme que nada é executado antes de clicar em **Pesquisar no Apify**.
|
|
391
|
-
6. **Pipeline > Criação de Vídeos:** teste primeiro o modo **Canal específico** e depois os modos de lote. O campo **Tópico ou briefing** pode ser escrito manualmente ou preenchido pelo botão **Gerar tópico/briefing com IA**; a geração só ocorre após o clique e usa o provider configurado em **Configuração API > API Keys**.
|
|
391
|
+
6. **Pipeline > Criação de Vídeos:** teste primeiro o modo **Canal específico** e depois os modos de lote. O campo **Tópico ou briefing** pode ser escrito manualmente ou preenchido pelo botão **Gerar tópico/briefing com IA**; a geração só ocorre após o clique e usa o provider configurado em **Configuração API > API Keys > Serviços e modelos**.
|
|
392
392
|
7. **Criação de Vídeos:** depois de seleccionar o canal, confirme que o painel entre **Canal** e **Estilo wide** mostra o Blueprint e a voz, ou **SEM BLUEPRINT CONFIGURADO**. Clique em **Gerar títulos e thumbnails com IA**, confirme pelo menos 20 candidatos de título e 3–5 variantes de thumbnail, escolha uma variante e verifique o prompt guardado. Na subaba **Vídeos**, verifique o estado `to_do`, o título escolhido, o estado da thumbnail e os botões **Iniciar** e **Parar**. Confirme também que **Criação de Músicas** mostra o mesmo fluxo com título próprio.
|
|
393
393
|
8. **Lote geral:** confirme que não existe **Canais incluídos** nem selector parcial. A UI deve listar todos os canais cadastrados e criar exactamente uma tarefa por canal, com briefing, título, thumbnail e Blueprint próprios. Use **Gerar tópicos individuais para todos os canais** e **Gerar títulos e thumbnails para todos os canais** antes de criar o batch.
|
|
394
394
|
9. **Upload:** configure o OAuth Client ID e Secret, autorize primeiro o **youtube-automation-agent** na própria aba, confirme o estado **pronto para publicar**, preencha título/descrição/tags e publique um MP4 real. O botão **Autorizar fallback OAuth** existe apenas para redundância; a Data API Key, se configurada, é exclusivamente para consultas oficiais públicas e nunca substitui OAuth.
|
|
395
395
|
10. **Edição > Limpador de Metadados:** suba um vídeo de terceiro, preencha título, preview, links, timestamps e tags, aplique a limpeza e descarregue a cópia limpa e o manifesto JSON. O original fica preservado e vídeos das páginas de criação não são aceites nesta área.
|
|
396
|
-
11. **Configurações > Configuração API:** confirme que
|
|
397
|
-
12. **OpenAI/ NVIDIA NIM:** em **LLM — providers e modelos**, seleccione `openai`, confirme a Base URL, introduza a API key e clique em **Consultar/actualizar modelos NIM**. Escolha um modelo da lista ou utilize o fallback manual e guarde as configurações. Confirme que a consulta só ocorre após o clique e que um erro de endpoint não interrompe a UI.
|
|
398
|
-
13. **Criação de Vídeos:** confirme que
|
|
396
|
+
11. **Configurações > Configuração API:** confirme que as credenciais ficam locais e não aparecem no Git. Em **API Keys > Fontes de materiais**, seleccione Pexels, Pixabay, Coverr, WaveSpeed AI, LoomLoom, TwelveLabs ou Ficheiros locais. Para uma fonte com API, preencha a primeira chave e use **Adicionar outra chave** para criar quantas linhas forem necessárias; clique em **Guardar fonte e chaves**. Cada fonte mantém a sua própria lista, com deduplicação e rotação interna. Não são pedidos endpoint, proxy, qualidade, codec, FFmpeg, Whisper, directório ou filtros nesta subaba. Na secção **Contas Google/YouTube — canais em lote**, adicione cada conta com o seu e-mail/Gmail, OAuth Client ID, OAuth Client Secret e `sessionInfo` próprios; use **Repetir campos para nova conta** para preparar contas adicionais e o ícone **Apagar conta** para eliminar individualmente a conta, tokens, documento JSON e associações de canais.
|
|
397
|
+
12. **OpenAI/ NVIDIA NIM:** em **API Keys > Serviços e modelos > LLM — providers e modelos**, seleccione `openai`, confirme a Base URL, introduza a API key e clique em **Consultar/actualizar modelos NIM**. Escolha um modelo da lista ou utilize o fallback manual e guarde as configurações. Confirme que a consulta só ocorre após o clique e que um erro de endpoint não interrompe a UI.
|
|
398
|
+
13. **Criação de Vídeos:** confirme que a fonte seleccionada em **API Keys > Fontes de materiais** é usada pela pipeline, que `Estilo IA` aparece apenas em `full_ia` e que `Apenas Música` exige áudio e guarda `background_mode=none`.
|
|
399
399
|
14. **Canais Youtube:** abra **Configurações > Canais Youtube**, clique em **Editar** num cartão, altere o nicho, Blueprint/Prompts do Canal, Narrador/Voz padrão e horário, guarde e confirme que o cartão é actualizado sem duplicar o canal. Clique em **Actualizar últimos 10 vídeos**, valide a vista Lista, mude para Kanban e abra **Editar vídeo** para testar título, estado, data, URL e notas.
|
|
400
400
|
15. **Automação Youtube:** no cartão de cada canal, escolha o **Blueprint padrão** e a **Voz padrão**, clique em `Guardar` e confirme que o resumo do cartão se actualiza. Configure também **Automação ON** e um horário `HH:MM`; confirme a lista de vídeos cadastrados. Os mesmos defaults aparecem em **Canais Youtube** e são usados em novas tarefas. Inicie o Thunderbolt pelo launcher, confirme o aviso verde **Worker activo** e verifique que o relógio apresentado corresponde ao computador. O worker cria no máximo um lote por canal por dia quando o horário local coincide.
|
|
401
401
|
16. **Configurações > Configuração API > Teste de vozes:** teste Edge/Azure ou provider configurado e confirme reprodução/download sem criação de tarefa.
|
|
@@ -404,6 +404,14 @@ Após iniciar a aplicação, valide o seguinte percurso:
|
|
|
404
404
|
19. **Configurações > MCP > Servidor MCP:** abra a subaba, mantenha o host `127.0.0.1`, active **Servidor MCP ON** e clique em **Guardar e iniciar Servidor MCP**. Confirme o endpoint `/mcp` e o health endpoint `/health`. Mantenha **Permitir ferramentas de escrita** desactivado até precisar que um agente crie lotes.
|
|
405
405
|
20. **Configurações > MCP > Skill:** clique em **Guardar skill localmente** e confirme o ficheiro em `storage/skills/moneyprinterturbo-video.md`; opcionalmente use **Descarregar skill .md** para obter a cópia através do navegador.
|
|
406
406
|
|
|
407
|
+
## Configuração API — fontes de materiais
|
|
408
|
+
|
|
409
|
+
A área **Configurações > Configuração API > API Keys** separa as credenciais de serviços da configuração de fontes. Abra a subaba **Fontes de materiais** e escolha a fonte que a pipeline deverá usar. Para **Pexels**, **Pixabay**, **Coverr**, **WaveSpeed AI**, **LoomLoom** ou **TwelveLabs**, cada linha **API Key** é mascarada. Clique em **Adicionar outra chave** para guardar várias credenciais da mesma fonte; por exemplo, duas chaves Pixabay e duas Pexels ficam em listas independentes.
|
|
410
|
+
|
|
411
|
+
Clique em **Guardar fonte e chaves** para persistir a fonte activa e as respectivas chaves no storage local. O Thunderbolt remove linhas vazias e duplicadas, mantém compatibilidade com instalações antigas que usavam campos separados e escreve arrays no `config.toml` do MoneyPrinterTurbo para permitir rotação interna. Ao seleccionar **Ficheiros locais**, nenhuma API key é necessária e os materiais são obtidos do storage local.
|
|
412
|
+
|
|
413
|
+
A subaba de fontes não é um painel de tuning: endpoints, proxy, qualidade, correspondência ao roteiro, directório, FFmpeg, codec, Whisper e outros parâmetros técnicos não são expostos ali. A subaba **Serviços e modelos** continua disponível para credenciais de LLM, TTS, Nano Banana, TikTok, Postiz e integrações que não são fontes de materiais. `INNERTUBE_API_KEY` não pertence a esta página; permanece no documento de cada conta em **Configurações > Contas Google**.
|
|
414
|
+
|
|
407
415
|
## Gestão de Canais, edição e vídeos recentes
|
|
408
416
|
|
|
409
417
|
Na página **Configurações > Canais Youtube**, cada canal cadastrado aparece num cartão com o botão **Editar**. O editor permite alterar o nome, URL, handle, idioma, estilo wide, **Canais de Referência / Nicho**, **Prompts do Canal** (Blueprint padrão), **Narrador** (Voz padrão), conta Google do Upload directo, descrição e Automação ON/horário. Guardar alterações actualiza o mesmo registo local; não é necessário apagar e criar o canal novamente.
|
|
@@ -428,11 +436,11 @@ Ao abrir a página, o Thunderbolt não prepara dados públicos, não descarrega
|
|
|
428
436
|
|
|
429
437
|
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.
|
|
430
438
|
|
|
431
|
-
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.2.
|
|
439
|
+
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.2.76 install`; o instalador detecta e reutiliza o que já estiver válido.
|
|
432
440
|
|
|
433
441
|
### Niche Finder Apify
|
|
434
442
|
|
|
435
|
-
A alternativa Apify não usa o dataset, parâmetros, execução ou estado da alternativa Kaggle. Configure o **Apify API Token**, o **Apify Actor ID**, o intervalo de consulta e o limite da execução em **Configuração API**. Na aba, informe até três palavras-chave, período, máximo de resultados, Shorts, duração, idioma das legendas, ordenação e filtros de legendas. O botão **Pesquisar no Apify** inicia manualmente o actor `streamers~youtube-scraper`, consulta o estado do run, carrega o dataset, normaliza os vídeos, limpa SRT, calcula VSC Ratio, tenta resumir transcrições através do LLM configurado e disponibiliza resultados JSON/CSV. O histórico resumido é guardado em `storage/state/niche_apify_runs.json`; não há gravação automática no Airtable do workflow anexado.
|
|
443
|
+
A alternativa Apify não usa o dataset, parâmetros, execução ou estado da alternativa Kaggle. Configure o **Apify API Token**, o **Apify Actor ID**, o intervalo de consulta e o limite da execução em **Configuração API > API Keys > Serviços e modelos**. Na aba, informe até três palavras-chave, período, máximo de resultados, Shorts, duração, idioma das legendas, ordenação e filtros de legendas. O botão **Pesquisar no Apify** inicia manualmente o actor `streamers~youtube-scraper`, consulta o estado do run, carrega o dataset, normaliza os vídeos, limpa SRT, calcula VSC Ratio, tenta resumir transcrições através do LLM configurado e disponibiliza resultados JSON/CSV. O histórico resumido é guardado em `storage/state/niche_apify_runs.json`; não há gravação automática no Airtable do workflow anexado.
|
|
436
444
|
|
|
437
445
|
## AI Influencers: Personagens, Redes Sociais e Tutorial Meta
|
|
438
446
|
|
|
@@ -454,7 +462,7 @@ No modo automático de **Cortes**, a aplicação não inventa uma selecção vir
|
|
|
454
462
|
|
|
455
463
|
## Criação de Vídeos — IA editorial e pacotes criativos
|
|
456
464
|
|
|
457
|
-
O formulário permite escrever manualmente o **Tópico ou briefing** ou clicar em **Gerar tópico/briefing com IA**. O botão usa o provider LLM, Base URL, API key e modelo guardados em **Configuração API > API Keys**, incorpora a descrição, o nicho e o Blueprint do canal e coloca o resultado no campo para revisão. Abrir a página não inicia chamadas externas.
|
|
465
|
+
O formulário permite escrever manualmente o **Tópico ou briefing** ou clicar em **Gerar tópico/briefing com IA**. O botão usa o provider LLM, Base URL, API key e modelo guardados em **Configuração API > API Keys > Serviços e modelos**, incorpora a descrição, o nicho e o Blueprint do canal e coloca o resultado no campo para revisão. Abrir a página não inicia chamadas externas.
|
|
458
466
|
|
|
459
467
|
Depois do tópico, clique em **Gerar títulos e thumbnails com IA**. O Thunderbolt solicita pelo menos 20 títulos candidatos e 3–5 variantes de thumbnail, aplica as regras das referências empacotadas em `seed/references/` e guarda a variante escolhida na task. O título aparece pré-preenchido no Upload. A thumbnail guarda conceito, texto de até quatro palavras, composição, paleta, prompt e sinergia com o título; se não existir um provider de imagem configurado, fica com estado **Prompt de thumbnail pronto — imagem pendente de provider de imagem**, sem criar um ficheiro artificial.
|
|
460
468
|
|
|
@@ -472,7 +480,7 @@ A área **Teste de vozes**, dentro de **Configurações > Configuração API**,
|
|
|
472
480
|
|
|
473
481
|
## Upload, Postiz e Upload directo
|
|
474
482
|
|
|
475
|
-
A subaba **Postiz**, dentro de **Upload**, permite carregar as integrações Postiz, seleccionar o canal ligado e enviar vídeos MP4 através da Public API. Configure **Activar Postiz como fallback final**, **Postiz API key**, **Postiz Public API Base URL**, **Postiz MCP URL** e, opcionalmente, o ID da integração padrão em **Configuração API > API Keys**. A API key é enviada como valor bruto do cabeçalho `Authorization`; o upload usa `POST /upload` e a publicação usa `POST /posts`.
|
|
483
|
+
A subaba **Postiz**, dentro de **Upload**, permite carregar as integrações Postiz, seleccionar o canal ligado e enviar vídeos MP4 através da Public API. Configure **Activar Postiz como fallback final**, **Postiz API key**, **Postiz Public API Base URL**, **Postiz MCP URL** e, opcionalmente, o ID da integração padrão em **Configuração API > API Keys > Serviços e modelos**. A API key é enviada como valor bruto do cabeçalho `Authorization`; o upload usa `POST /upload` e a publicação usa `POST /posts`.
|
|
476
484
|
|
|
477
485
|
O botão de envio YouTube segue a ordem fixa **1. API Oficial, 2. Upload directo, 3. Postiz**. A API Oficial regista no storage local até cinco envios bem-sucedidos por dia por conta Gmail. Quando a quota é atingida ou o método falha, o Thunderbolt valida o `credentials.json` e tenta o Upload directo. Postiz só é tentado como último recurso e apenas quando está activo, com API key e integração válida.
|
|
478
486
|
|
|
@@ -498,7 +506,7 @@ A aba **Limpador de Metadados** recebe somente vídeos externos já prontos. O f
|
|
|
498
506
|
|
|
499
507
|
A descrição combina **Preview**, **Links** e **Timestamps**, seguindo a estrutura do workflow `YTBMetadataGenerator.json`. O preview recomendado tem entre 100 e 200 caracteres; os capítulos devem começar por `00:00`. O manifesto JSON descarregado contém os campos para um fluxo posterior de upload. Esta adaptação local não faz scraping automático via RSS/Apify nem publica directamente no YouTube.
|
|
500
508
|
|
|
501
|
-
Se a limpeza falhar,
|
|
509
|
+
Se a limpeza falhar, execute `doctor` para confirmar a disponibilidade do FFmpeg e valide a instalação de `imageio-ffmpeg`; o caminho técnico é detectado internamente e não é configurado na subaba de fontes. O processo usa `-map_metadata -1` para remover os metadados existentes e `-c copy` para evitar uma re-encodificação desnecessária sempre que o contentor permitir.
|
|
502
510
|
|
|
503
511
|
## 11. Problemas frequentes
|
|
504
512
|
|
package/README.md
CHANGED
|
@@ -22,12 +22,12 @@ A primeira versão implementa a camada UI independente com:
|
|
|
22
22
|
| Edição | Menu expansível abaixo de Automação com Limpador de Metadados, Clip Generator local em Cortes e Editor Python inspirado no PYEdit para vídeos e scripts locais |
|
|
23
23
|
| Upload | YouTube via `youtube-automation-agent` adaptado internamente, fallback ordenado API Oficial → Upload directo → Postiz, upload de MP4 para Postiz via API key/MCP configurável, TikTok, Instagram e Facebook Pages no front end |
|
|
24
24
|
| MCP | Catálogo local opcional de Short Video Maker, AutoVio, OpenMontage e OpenCut, com portas editáveis e activação |
|
|
25
|
-
| Configurações
|
|
25
|
+
| Configurações | **Contas Google**, **Configuração API** e **Notificações**; API Keys divididas entre **Serviços e modelos** e **Fontes de materiais**, com várias chaves independentes por fonte, além de contas Google/YouTube por cartão, `INNERTUBE_API_KEY` no documento da conta, providers, TTS, Nano Banana, Postiz, TikTok e Upload-Post |
|
|
26
26
|
| Launcher | Execução via `npx`, instalação assistida, diagnóstico e preparação para distribuição |
|
|
27
27
|
|
|
28
28
|
## Upload directo — credenciais por conta e por canal
|
|
29
29
|
|
|
30
|
-
O Upload directo baseado no [YouTube-Video-Upload-Frontend-Api](https://github.com/Nojus10/YouTube-Video-Upload-Frontend-Api) usa um único documento JSON por conta Google. Em **Configurações
|
|
30
|
+
O Upload directo baseado no [YouTube-Video-Upload-Frontend-Api](https://github.com/Nojus10/YouTube-Video-Upload-Frontend-Api) usa um único documento JSON por conta Google. Em **Configurações > Contas Google > Contas Google/YouTube — canais em lote**, cada conta aparece como um cartão expansível identificado por **nome — e-mail**. Dentro do cartão existe o uploader **Subir documento de cookies/credenciais** e o único campo técnico permitido na UI, **sessionInfo token desta conta Google**. Cookies, `INNERTUBE_API_KEY`, `chunk_size` e `delegated_session_ids` continuam exclusivamente no documento.
|
|
31
31
|
|
|
32
32
|
O botão **Adicionar outra conta Gmail** fica abaixo de uma divisória, fora dos cartões existentes. Ao criar uma conta, o Thunderbolt cria imediatamente `credentials.json` com placeholders vazios. Um upload completo ou parcial é incorporado por merge: os cookies enviados actualizam apenas os cookies fornecidos e valores como `sessionInfo`, `INNERTUBE_API_KEY`, `chunk_size` e `delegated_session_ids` existentes não são apagados. O alerta **Documento incompleto: SID, SSID, HSID, APISID, SAPISID, sessionInfo, INNERTUBE_API_KEY** permanece visível por conta quando necessário.
|
|
33
33
|
|
|
@@ -38,11 +38,15 @@ Os dados são segredos de sessão. Os valores não aparecem em tabelas ou logs,
|
|
|
38
38
|
|
|
39
39
|
Os adaptadores do MoneyPrinterTurbo e de publicação nas plataformas são ligados pelas configurações locais e pelos pontos de integração em `integrations/`. A UI não inventa dados quando um serviço externo ou credencial não está disponível.
|
|
40
40
|
|
|
41
|
-
## Navegação da UI 0.2.
|
|
41
|
+
## Navegação da UI 0.2.76
|
|
42
42
|
|
|
43
43
|
A barra lateral mantém os níveis principais, nesta ordem: **Início**, **Niche Finder**, **Pipeline**, **Pipeline TikTok**, **Automação**, **Edição**, **AI Influencers** e **Configurações**. **Pipeline** é expansível e contém **Criação de Vídeos**, **Criação de Músicas**, **Roteiros** e **Upload**. **Automação** também é expansível e contém **Automação Youtube**. **Edição** é expansível e contém **Limpador de Metadados**, **Cortes**, **Editor Python** e **Download Mídia**, nessa ordem. **AI Influencers** é expansível e contém **Personagens**, **Redes Sociais** e **Tutorial Meta**, nessa ordem. **Niche Finder** é expansível e contém **Niche Finder Kaggle** e **Niche Finder Apify**. **Configurações** é expansível e contém **Canais Youtube**, **Blueprints Youtube**, **MCP**, **Contas Google**, **Configuração API** e **Notificações**. O Início reúne o dashboard e as filas do Pipeline, sem botões de acções rápidas.
|
|
44
44
|
|
|
45
|
-
Dentro de **Configurações > Contas Google**, a UI contém os cartões expansíveis de contas Google/YouTube, `sessionInfo`, documentos de Upload directo, `INNERTUBE_API_KEY`, o formulário **Adicionar outra conta Gmail** e a configuração global do YouTube (OAuth Client ID, OAuth Client Secret e YouTube Data API Key). A página **Configuração API** contém as restantes
|
|
45
|
+
Dentro de **Configurações > Contas Google**, a UI contém os cartões expansíveis de contas Google/YouTube, `sessionInfo`, documentos de Upload directo, `INNERTUBE_API_KEY`, o formulário **Adicionar outra conta Gmail** e a configuração global do YouTube (OAuth Client ID, OAuth Client Secret e YouTube Data API Key). A página **Configuração API** contém as restantes credenciais, providers, modelos, serviços, Nano Banana, TikTok e Postiz.
|
|
46
|
+
|
|
47
|
+
Em **Configuração API > API Keys**, a subaba **Fontes de materiais** segue o padrão do MoneyPrinterTurbo: seleccione uma fonte — **Pexels**, **Pixabay**, **Coverr**, **WaveSpeed AI**, **LoomLoom**, **TwelveLabs** ou **Ficheiros locais** — e introduza as API keys dessa fonte. **Adicionar outra chave** cria outra linha para a mesma fonte; as listas são guardadas separadamente por fonte, deduplicadas e exportadas para o `config.toml` do motor como arrays para rotação. A subaba não expõe endpoint, proxy, qualidade, codec, FFmpeg, Whisper, directório ou filtros técnicos: esses valores permanecem internalizados e a fonte local não requer credencial.
|
|
48
|
+
|
|
49
|
+
A subaba **Serviços e modelos** mantém as credenciais de providers e serviços que não são fontes de materiais. A chave `gemini_image_api_key` da Nano Banana continua separada de `gemini_api_key` do LLM textual. `INNERTUBE_API_KEY` permanece exclusivamente em **Configurações > Contas Google**, dentro da configuração da conta, e não é duplicada em API Keys.
|
|
46
50
|
|
|
47
51
|
A página **AI Influencers > Tutorial Meta** apresenta o guia de configuração de uma conta Instagram profissional e das credenciais Meta para automações com n8n, distribuído localmente em `seed/references/guide-instagram.md` e com ligação para a [fonte original no GitHub](https://github.com/gyoridavid/ai_agents_az/blob/main/episode_8/guide-instagram.md). A página **Configurações > Notificações** mantém um histórico persistente de conclusões e falhas, reconcilia estados escritos por componentes locais e disponibiliza um checkbox independente para cada operação mapeada.
|
|
48
52
|
|
|
@@ -58,7 +62,7 @@ Abaixo do cartão, a secção **Últimos 10 vídeos publicados** usa o feed púb
|
|
|
58
62
|
|
|
59
63
|
## Criação de Vídeos — geração editorial por canal
|
|
60
64
|
|
|
61
|
-
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 **
|
|
65
|
+
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.
|
|
62
66
|
|
|
63
67
|
Entre **Canal** e **Estilo wide**, a UI mostra o Blueprint padrão resolvido do canal e a voz associada. Quando não existe configuração, apresenta **SEM BLUEPRINT CONFIGURADO**. 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.
|
|
64
68
|
|
|
@@ -68,7 +72,7 @@ No modo **Lote geral**, o Thunderbolt remove a selecção parcial e processa aut
|
|
|
68
72
|
|
|
69
73
|
## OpenAI/ NVIDIA NIM — descoberta de modelos
|
|
70
74
|
|
|
71
|
-
Em **
|
|
75
|
+
Em **Configuração API > API Keys > Serviços e modelos > LLM — providers e modelos**, a área anteriormente chamada **OpenAI — API key, Base URL e modelo** passa a chamar-se **OpenAI/ NVIDIA NIM — API key, Base URL e modelo**. O campo de modelo manual usa o parâmetro `help` suportado pelas versões do Streamlit instaladas pelo Thunderbolt, evitando o erro de argumento `help_text`. O provider interno continua a ser `openai`, para preservar a compatibilidade com o MoneyPrinterTurbo e com os restantes endpoints OpenAI-compatible.
|
|
72
76
|
|
|
73
77
|
A Base URL predefinida para NVIDIA NIM é `https://integrate.api.nvidia.com/v1`. Depois de inserir a API key, clique em **Consultar/actualizar modelos NIM**. O Thunderbolt consulta a Base URL acrescentando `/models`, lê os identificadores do formato OpenAI (`data[].id`) e apresenta-os num selector. O modelo escolhido é guardado em `openai_model_name` e é sincronizado para o `config.toml` do motor.
|
|
74
78
|
|
|
@@ -165,9 +169,9 @@ thunderbolt
|
|
|
165
169
|
No Windows PowerShell, se `npx` for bloqueado por `npx.ps1`, use directamente `npx.cmd`:
|
|
166
170
|
|
|
167
171
|
```powershell
|
|
168
|
-
npx.cmd --yes @danhachuel/thunderbolt@0.2.
|
|
169
|
-
npx.cmd --yes @danhachuel/thunderbolt@0.2.
|
|
170
|
-
npx.cmd --yes @danhachuel/thunderbolt@0.2.
|
|
172
|
+
npx.cmd --yes @danhachuel/thunderbolt@0.2.76 install
|
|
173
|
+
npx.cmd --yes @danhachuel/thunderbolt@0.2.76 doctor
|
|
174
|
+
npx.cmd --yes @danhachuel/thunderbolt@0.2.76
|
|
171
175
|
```
|
|
172
176
|
|
|
173
177
|
Como alternativa, pode permitir scripts para o seu utilizador:
|
|
@@ -191,13 +195,13 @@ O menu expansível **Niche Finder** contém duas alternativas independentes. **N
|
|
|
191
195
|
|
|
192
196
|
A interface apresenta dentro da aba os parâmetros da busca: número de clusters entre 2 e 10, suporte mínimo entre 0,01 e 0,50, país, categoria de engagement, intervalo de datas e tags. A página permanece sem resultados até o primeiro clique em **Analisar Nichos**; depois, se os parâmetros forem alterados, mostra os resultados anteriores e pede novo clique para aplicar os filtros actuais. O núcleo aplica normalização, filtros, `log1p`, `StandardScaler`, K-Means e FP-Growth. Os resultados aparecem em DataFrames para clusters, itemsets frequentes, regras de associação e dados analisados, acompanhados por uma visualização Plotly nativa e pesquisa de palavras nos clusters.
|
|
193
197
|
|
|
194
|
-
**Niche Finder Apify** é a segunda alternativa e não usa o dataset, filtros, execução ou resultados Kaggle. Define três palavras-chave, período, limite de resultados, Shorts, duração, idioma de legendas e ordenação; depois de clicar em **Pesquisar no Apify**, inicia o actor `streamers~youtube-scraper`, acompanha o run, carrega o dataset, normaliza vídeos, limpa SRT, calcula VSC Ratio e tenta resumir as transcrições com o provider LLM configurado. Os resultados ficam na sessão própria `niche_apify_results`, o histórico pequeno fica em `storage/state/niche_apify_runs.json` e existem exportações JSON/CSV. Configure o **Apify API Token** em
|
|
198
|
+
**Niche Finder Apify** é a segunda alternativa e não usa o dataset, filtros, execução ou resultados Kaggle. Define três palavras-chave, período, limite de resultados, Shorts, duração, idioma de legendas e ordenação; depois de clicar em **Pesquisar no Apify**, inicia o actor `streamers~youtube-scraper`, acompanha o run, carrega o dataset, normaliza vídeos, limpa SRT, calcula VSC Ratio e tenta resumir as transcrições com o provider LLM configurado. Os resultados ficam na sessão própria `niche_apify_results`, o histórico pequeno fica em `storage/state/niche_apify_runs.json` e existem exportações JSON/CSV. Configure o **Apify API Token** em **Configuração API > API Keys > Serviços e modelos**. As dependências adicionais são instaladas pelo fluxo normal do pacote: `requests`, `pandas` e os componentes já existentes da análise. Em instalações existentes, execute novamente `npx.cmd --yes @danhachuel/thunderbolt@0.2.76 install`; o instalador detecta e reutiliza componentes já válidos.
|
|
195
199
|
|
|
196
200
|
## Editor Python baseado no PYEdit
|
|
197
201
|
|
|
198
202
|
O **Editor Python** combina o recorte seguro do [PYEdit](https://github.com/Congren/PYEdit) com o armazenamento local do Thunderbolt. Na subaba **Vídeos**, pode escolher um vídeo já gerado e registado nos artefactos da pipeline, indicar qualquer pasta local de vídeos ou fazer upload manual. A página não inicia nenhuma edição ao ser aberta.
|
|
199
203
|
|
|
200
|
-
As operações disponíveis são cortar trecho, remover áudio, extrair áudio, substituir áudio, alterar velocidade e redimensionar vídeo. Cada operação cria uma cópia em `storage/python_editor/outputs/`, preserva o original, regista um histórico separado em `storage/state/python_editor_edits.json` e permite descarregar o resultado e um manifesto JSON. As operações usam FFmpeg local, aproveitando
|
|
204
|
+
As operações disponíveis são cortar trecho, remover áudio, extrair áudio, substituir áudio, alterar velocidade e redimensionar vídeo. Cada operação cria uma cópia em `storage/python_editor/outputs/`, preserva o original, regista um histórico separado em `storage/state/python_editor_edits.json` e permite descarregar o resultado e um manifesto JSON. As operações usam FFmpeg local, aproveitando a detecção interna e o fallback `imageio-ffmpeg`; a subaba de fontes não expõe caminhos técnicos.
|
|
201
205
|
|
|
202
206
|
Na subaba **Código Python**, pode criar e guardar scripts em `storage/python_editor/scripts/` ou carregar scripts existentes. Por segurança, a UI não executa código Python, não possui botão de execução e não executa scripts automaticamente.
|
|
203
207
|
|
|
@@ -284,7 +288,7 @@ O agente musical guarda os ficheiros em `storage/music/`, aceita formatos de áu
|
|
|
284
288
|
|
|
285
289
|
## Teste de vozes
|
|
286
290
|
|
|
287
|
-
A área **Configurações >
|
|
291
|
+
A área **Configurações > Configuração API > Teste de vozes** é isolada da pipeline.
|
|
288
292
|
Permite escolher Edge/Azure Speech ou um provider HTTP configurado, seleccionar voz/ID, alterar a velocidade, sintetizar uma amostra, reproduzi-la e descarregá-la. O áudio é guardado em `storage/voice_previews/` e nunca altera vídeos ou tarefas. Se uma instalação antiga não tiver `edge-tts`, execute `npx.cmd --yes @danhachuel/thunderbolt install`; o instalador detecta a dependência em falta sem reinstalar componentes válidos.
|
|
289
293
|
|
|
290
294
|
## MCP e integrações externas opcionais
|
|
@@ -302,7 +306,7 @@ As portas iniciais são `3123` para Short Video Maker, `3001` para a API backend
|
|
|
302
306
|
|
|
303
307
|
A subaba **Vídeos**, dentro de **Criação de Vídeos** ou **Criação de Músicas**, mostra o backlog e permite iniciar/parar tarefas; deixou de existir como botão principal na barra lateral.
|
|
304
308
|
|
|
305
|
-
|
|
309
|
+
As credenciais do TikTok ficam em **Configuração API > API Keys > Serviços e modelos**, no expander próprio de TikTok.
|
|
306
310
|
Para YouTube, o bloco principal pede o OAuth Client ID e Client Secret; a Data API Key, se usada, fica numa área opcional separada. Client ID + Client Secret não formam uma Data API Key nem um token OAuth.
|
|
307
311
|
Redirect URI, scopes, autorização OAuth e tokens são geridos no TikTok for Developers Playground. O adaptador rejeita o upload quando faltam credenciais ou OAuth, em vez de indicar sucesso falso.
|
|
308
312
|
|
package/app/main.py
CHANGED
|
@@ -33,6 +33,7 @@ from hermes_ui.python_editor import AUDIO_EXTENSIONS, VIDEO_EXTENSIONS, PythonEd
|
|
|
33
33
|
from hermes_ui.cuts import CutsError, download_direct_video_url, generate_clips, list_generated_videos as list_cut_generated_videos, list_runs as list_cut_runs, list_video_files as list_cut_video_files, manifest_bytes as cut_manifest_bytes, store_uploaded_video, zip_run as zip_cut_run
|
|
34
34
|
from hermes_ui.mcp import detect_local_service, install_skill_locally, load_integrations, load_server_config, read_packaged_skill, save_server_config, update_integration
|
|
35
35
|
from hermes_ui.mcp_server import server_status, start_server, stop_server
|
|
36
|
+
from hermes_ui.material_sources import material_api_keys, material_source_catalog, selected_material_source, update_material_api_keys
|
|
36
37
|
from hermes_ui.music import list_music_files, materialize_suno_audio, request_suno_generation, store_music_file
|
|
37
38
|
from hermes_ui.media_downloader import AUDIO_FORMATS, VIDEO_CONTAINERS, VIDEO_QUALITY_OPTIONS, MediaDownloadError, build_download_options, clear_media_download_history, dependency_status, download_media, list_media_downloads, media_download_file
|
|
38
39
|
from hermes_ui.notifications import clear_notifications, list_notifications, mark_all_notifications_read, mark_notification_read, notification_event_catalog, notification_preferences, record_notification, reconcile_persisted_notifications, save_notification_preferences, unread_notification_count
|
|
@@ -3238,6 +3239,51 @@ def render_google_accounts():
|
|
|
3238
3239
|
st.success("Configuração global do YouTube guardada em Contas Google.")
|
|
3239
3240
|
st.rerun()
|
|
3240
3241
|
|
|
3242
|
+
def render_material_source_api_keys(settings: dict[str, Any]) -> None:
|
|
3243
|
+
st.subheader("Fontes de materiais")
|
|
3244
|
+
st.caption("Seleccione a fonte que será usada na pipeline e guarde uma ou mais API keys. Qualidade, endpoints e parâmetros internos são definidos pelo Thunderbolt.")
|
|
3245
|
+
source_catalog = material_source_catalog()
|
|
3246
|
+
source_codes = [item["code"] for item in source_catalog] + ["local"]
|
|
3247
|
+
source_labels = {item["code"]: item["label"] for item in source_catalog} | {"local": "Ficheiros locais"}
|
|
3248
|
+
source_help = {item["code"]: item["description"] for item in source_catalog} | {"local": "Usar materiais já existentes no storage local; não requer API key."}
|
|
3249
|
+
selected_source = st.selectbox(
|
|
3250
|
+
"Fonte de materiais",
|
|
3251
|
+
source_codes,
|
|
3252
|
+
index=source_codes.index(selected_material_source(settings)) if selected_material_source(settings) in source_codes else 0,
|
|
3253
|
+
format_func=lambda value: source_labels.get(value, value),
|
|
3254
|
+
key="material_source_selector",
|
|
3255
|
+
)
|
|
3256
|
+
st.caption(source_help.get(selected_source, ""))
|
|
3257
|
+
current_keys = material_api_keys(settings, selected_source)
|
|
3258
|
+
row_key = f"material_source_key_rows_{selected_source}"
|
|
3259
|
+
row_count = max(len(current_keys), int(st.session_state.get(row_key, len(current_keys) or 1)))
|
|
3260
|
+
if selected_source == "local":
|
|
3261
|
+
st.info("A fonte local não usa API key. Os materiais devem existir na pasta configurada do storage.")
|
|
3262
|
+
if st.button("Guardar fonte local", type="primary", key="save_local_material_source"):
|
|
3263
|
+
settings["video_source"] = selected_source
|
|
3264
|
+
write_json("settings.json", settings)
|
|
3265
|
+
st.success("Fonte de materiais guardada: ficheiros locais.")
|
|
3266
|
+
st.rerun()
|
|
3267
|
+
return
|
|
3268
|
+
with st.form(f"material_api_keys_form_{selected_source}"):
|
|
3269
|
+
key_values: list[str] = []
|
|
3270
|
+
for index in range(row_count):
|
|
3271
|
+
key_values.append(st.text_input(f"API Key {index + 1}", value=current_keys[index] if index < len(current_keys) else "", type="password", key=f"material_api_key_{selected_source}_{index}"))
|
|
3272
|
+
save_keys = st.form_submit_button("Guardar fonte e chaves", type="primary", use_container_width=True)
|
|
3273
|
+
add_key = st.form_submit_button("Adicionar outra chave", use_container_width=True)
|
|
3274
|
+
if add_key:
|
|
3275
|
+
st.session_state[row_key] = row_count + 1
|
|
3276
|
+
st.rerun()
|
|
3277
|
+
if save_keys:
|
|
3278
|
+
update_material_api_keys(settings, selected_source, key_values)
|
|
3279
|
+
settings["video_source"] = selected_source
|
|
3280
|
+
write_json("settings.json", settings)
|
|
3281
|
+
st.session_state[row_key] = max(1, len(material_api_keys(settings, selected_source)))
|
|
3282
|
+
st.success(f"Fonte {source_labels.get(selected_source, selected_source)} guardada com {len(material_api_keys(settings, selected_source))} chave(s).")
|
|
3283
|
+
st.rerun()
|
|
3284
|
+
st.caption(f"{len(current_keys)} chave(s) actualmente guardada(s) para {source_labels.get(selected_source, selected_source)}. As chaves são mantidas no storage local e usadas com rotação interna.")
|
|
3285
|
+
|
|
3286
|
+
|
|
3241
3287
|
def render_settings():
|
|
3242
3288
|
st.title("Configuração API")
|
|
3243
3289
|
st.caption("Configuração das APIs, providers, serviços e ferramentas técnicas usados pelo Thunderbolt. As credenciais ficam no storage local e não são enviadas para o GitHub.")
|
|
@@ -3255,221 +3301,191 @@ def render_settings():
|
|
|
3255
3301
|
api_keys_tab, voice_test_tab = st.tabs(["API Keys", "Teste de vozes"])
|
|
3256
3302
|
|
|
3257
3303
|
with api_keys_tab:
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
st.
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
with
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
cached_models = list(cached_catalog.get("models", [])) if cached_catalog.get("key") == catalog_key else []
|
|
3321
|
-
current_model_name = str(settings.get("openai_model_name", "") or "")
|
|
3322
|
-
manual_option = "__manual_model__"
|
|
3323
|
-
if cached_models:
|
|
3324
|
-
model_options = [manual_option, *cached_models]
|
|
3325
|
-
model_index = model_options.index(current_model_name) if current_model_name in model_options else 0
|
|
3326
|
-
selected_model = st.selectbox("Modelo OpenAI/ NVIDIA NIM", model_options, index=model_index, format_func=lambda value: "Escrever modelo manualmente" if value == manual_option else value, key="settings_openai_model_select")
|
|
3327
|
-
if selected_model == manual_option:
|
|
3328
|
-
openai_model_name = st.text_input("Modelo manual", value=current_model_name if current_model_name not in cached_models else "", help="Ex.: nvidia_nim/minimaxai/minimax-m3", key="settings_openai_model_manual")
|
|
3304
|
+
api_service_tab, material_sources_tab = st.tabs(["Serviços e modelos", "Fontes de materiais"])
|
|
3305
|
+
with api_service_tab:
|
|
3306
|
+
with st.form("settings_form"):
|
|
3307
|
+
st.subheader("API Keys")
|
|
3308
|
+
port = st.number_input("Porta Streamlit", 1, 65535, int(settings.get("port", 3030)))
|
|
3309
|
+
moneyprinter_path = st.text_input("Pasta do motor de vídeo", settings.get("moneyprinter_path", ""), key="settings_moneyprinter_path")
|
|
3310
|
+
with st.expander("Niche Finder — execução remota no Kaggle", expanded=True):
|
|
3311
|
+
st.caption("O dataset permanece no Kaggle. O Thunderbolt usa estas credenciais apenas para publicar/executar a kernel e obter os resultados pequenos da análise.")
|
|
3312
|
+
kaggle_cols = st.columns(3)
|
|
3313
|
+
with kaggle_cols[0]:
|
|
3314
|
+
kaggle_username = text_setting("Kaggle Username", "kaggle_username", help_text="Nome de utilizador da sua conta Kaggle, sem @ e sem URL.")
|
|
3315
|
+
with kaggle_cols[1]:
|
|
3316
|
+
kaggle_api_key = text_setting("Kaggle API Key", "kaggle_api_key", secret=True, help_text="Chave criada em Kaggle > Settings > API. Nunca é incluída no notebook ou no GitHub.")
|
|
3317
|
+
with kaggle_cols[2]:
|
|
3318
|
+
kaggle_kernel_slug = text_setting("Slug da kernel", "kaggle_kernel_slug", help_text="Identificador da kernel remota, por exemplo thunderbolt-niche-finder.")
|
|
3319
|
+
|
|
3320
|
+
with st.expander("Niche Finder — execução através da Apify", expanded=True):
|
|
3321
|
+
st.caption("O token fica guardado apenas no storage local. A aba Niche Finder Apify só usa este serviço depois de clicar no botão de pesquisa.")
|
|
3322
|
+
apify_cols = st.columns(4)
|
|
3323
|
+
with apify_cols[0]:
|
|
3324
|
+
apify_api_token = text_setting("Apify API Token", "apify_api_token", secret=True, help_text="Token pessoal da Apify. Não é incluído no workflow, logs ou GitHub.")
|
|
3325
|
+
with apify_cols[1]:
|
|
3326
|
+
apify_actor_id = text_setting("Apify Actor ID", "apify_actor_id", help_text="Por padrão: streamers~youtube-scraper.")
|
|
3327
|
+
with apify_cols[2]:
|
|
3328
|
+
apify_poll_interval = st.number_input("Intervalo de consulta (s)", min_value=1, max_value=120, value=int(settings.get("apify_poll_interval_seconds", 10)), step=1)
|
|
3329
|
+
with apify_cols[3]:
|
|
3330
|
+
apify_run_timeout = st.number_input("Limite da execução (s)", min_value=30, max_value=7200, value=int(settings.get("apify_run_timeout_seconds", 900)), step=30)
|
|
3331
|
+
|
|
3332
|
+
with st.expander("Nano Banana — geração de thumbnails", expanded=True):
|
|
3333
|
+
st.caption("A Nano Banana gera a imagem final das thumbnails a partir da variante escolhida. A chave é guardada apenas no storage local e é distinta da chave do Gemini usado como LLM textual.")
|
|
3334
|
+
nano_cols = st.columns(2)
|
|
3335
|
+
with nano_cols[0]:
|
|
3336
|
+
gemini_image_api_key = text_setting("Nano Banana API key", "gemini_image_api_key", secret=True, help_text="Chave criada no Google AI Studio para a API Gemini. Nunca é incluída no código, logs ou pacote.")
|
|
3337
|
+
gemini_image_model = st.selectbox("Modelo Nano Banana", ["gemini-3.1-flash-image", "gemini-3-pro-image", "gemini-2.5-flash-image"], index=["gemini-3.1-flash-image", "gemini-3-pro-image", "gemini-2.5-flash-image"].index(str(settings.get("gemini_image_model") or "gemini-3.1-flash-image")) if str(settings.get("gemini_image_model") or "gemini-3.1-flash-image") in {"gemini-3.1-flash-image", "gemini-3-pro-image", "gemini-2.5-flash-image"} else 0)
|
|
3338
|
+
with nano_cols[1]:
|
|
3339
|
+
gemini_image_aspect_ratio = st.selectbox("Proporção da thumbnail", ["16:9", "9:16", "1:1", "4:5"], index=["16:9", "9:16", "1:1", "4:5"].index(str(settings.get("gemini_image_aspect_ratio") or "16:9")) if str(settings.get("gemini_image_aspect_ratio") or "16:9") in {"16:9", "9:16", "1:1", "4:5"} else 0)
|
|
3340
|
+
gemini_image_size = st.selectbox("Tamanho da imagem", ["1K", "2K", "4K"], index=["1K", "2K", "4K"].index(str(settings.get("gemini_image_size") or "1K")) if str(settings.get("gemini_image_size") or "1K") in {"1K", "2K", "4K"} else 0)
|
|
3341
|
+
|
|
3342
|
+
with st.expander("LLM — providers e modelos", expanded=True):
|
|
3343
|
+
provider_options = ["moonshot", "shengsuanyun", "openai", "gemini", "deepseek", "qwen", "azure", "volcengine", "grok", "minimax", "mimo", "cloudflare", "modelscope", "aihubmix", "aimlapi", "evolink", "ollama", "oneapi", "litellm", "groq", "pollinations"]
|
|
3344
|
+
llm_provider = st.selectbox("LLM provider", provider_options, index=provider_options.index(settings.get("llm_provider", "moonshot")) if settings.get("llm_provider", "moonshot") in provider_options else 0)
|
|
3345
|
+
st.markdown("**OpenAI/ NVIDIA NIM — API key, Base URL e modelo**")
|
|
3346
|
+
st.caption("O provider interno continua a ser `openai`, mas pode usar qualquer endpoint OpenAI-compatible. Para NVIDIA NIM, a Base URL predefinida é `https://integrate.api.nvidia.com/v1`; o selector consulta `/models` e deixa um campo manual como fallback.")
|
|
3347
|
+
openai_cols = st.columns(3)
|
|
3348
|
+
with openai_cols[0]:
|
|
3349
|
+
openai_api_key = text_setting("OpenAI/ NVIDIA NIM API key", "openai_api_key", secret=True, help_text="API key do OpenAI ou do NVIDIA Build/NIM. A credencial fica apenas no storage local.")
|
|
3350
|
+
with openai_cols[1]:
|
|
3351
|
+
openai_base_url = st.text_input("OpenAI/ NVIDIA NIM Base URL", value=str(settings.get("openai_base_url", "") or DEFAULT_NVIDIA_NIM_BASE_URL), help="Ex.: https://integrate.api.nvidia.com/v1. O Thunderbolt acrescenta /models para descobrir os modelos.", key="settings_openai_base_url")
|
|
3352
|
+
with openai_cols[2]:
|
|
3353
|
+
cached_catalog = st.session_state.get("openai_model_catalog", {})
|
|
3354
|
+
catalog_key = f"{openai_base_url.strip()}::{hashlib.sha256(openai_api_key.encode('utf-8')).hexdigest()}"
|
|
3355
|
+
cached_models = list(cached_catalog.get("models", [])) if cached_catalog.get("key") == catalog_key else []
|
|
3356
|
+
current_model_name = str(settings.get("openai_model_name", "") or "")
|
|
3357
|
+
manual_option = "__manual_model__"
|
|
3358
|
+
if cached_models:
|
|
3359
|
+
model_options = [manual_option, *cached_models]
|
|
3360
|
+
model_index = model_options.index(current_model_name) if current_model_name in model_options else 0
|
|
3361
|
+
selected_model = st.selectbox("Modelo OpenAI/ NVIDIA NIM", model_options, index=model_index, format_func=lambda value: "Escrever modelo manualmente" if value == manual_option else value, key="settings_openai_model_select")
|
|
3362
|
+
if selected_model == manual_option:
|
|
3363
|
+
openai_model_name = st.text_input("Modelo manual", value=current_model_name if current_model_name not in cached_models else "", help="Ex.: nvidia_nim/minimaxai/minimax-m3", key="settings_openai_model_manual")
|
|
3364
|
+
else:
|
|
3365
|
+
openai_model_name = selected_model
|
|
3329
3366
|
else:
|
|
3330
|
-
openai_model_name =
|
|
3367
|
+
openai_model_name = st.text_input("Modelo OpenAI/ NVIDIA NIM", value=current_model_name, help="Pode escrever um ID manualmente se o endpoint não disponibilizar /models.", key="settings_openai_model_name")
|
|
3368
|
+
refresh_openai_models = st.form_submit_button("Consultar/actualizar modelos NIM", use_container_width=True)
|
|
3369
|
+
if cached_catalog.get("key") == catalog_key and cached_catalog.get("error"):
|
|
3370
|
+
st.warning(str(cached_catalog["error"]))
|
|
3371
|
+
elif cached_models:
|
|
3372
|
+
st.caption(f"{len(cached_models)} modelo(s) carregado(s) a partir de {openai_base_url.rstrip('/')}/models.")
|
|
3331
3373
|
else:
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3374
|
+
st.info("Preencha a API key e clique em Consultar/actualizar modelos NIM para carregar os IDs disponíveis.")
|
|
3375
|
+
llm_fields = [
|
|
3376
|
+
("Moonshot / Kimi", "moonshot", True), ("Shengsuan Cloud", "shengsuanyun", True),
|
|
3377
|
+
("Google Gemini", "gemini", True), ("DeepSeek", "deepseek", True), ("Alibaba Qwen", "qwen", True),
|
|
3378
|
+
("Azure OpenAI", "azure", True), ("VolcEngine Ark", "volcengine", True), ("xAI Grok", "grok", True),
|
|
3379
|
+
("MiniMax", "minimax", True), ("Xiaomi MiMo", "mimo", True), ("Cloudflare AI Gateway", "cloudflare", True),
|
|
3380
|
+
("ModelScope", "modelscope", True), ("AIHubMix", "aihubmix", True), ("AIML API", "aimlapi", True),
|
|
3381
|
+
("EvoLink", "evolink", True), ("Ollama", "ollama", False), ("OneAPI", "oneapi", True),
|
|
3382
|
+
("LiteLLM", "litellm", False), ("Groq", "groq", True), ("Pollinations AI", "pollinations", True),
|
|
3383
|
+
]
|
|
3384
|
+
for label, prefix, has_key in llm_fields:
|
|
3385
|
+
st.markdown(f"**{label}**")
|
|
3386
|
+
cols = st.columns(3)
|
|
3387
|
+
with cols[0]:
|
|
3388
|
+
if has_key:
|
|
3389
|
+
settings[f"{prefix}_api_key"] = text_setting("API key", f"{prefix}_api_key", secret=True)
|
|
3390
|
+
else:
|
|
3391
|
+
settings[f"{prefix}_api_key"] = settings.get(f"{prefix}_api_key", "")
|
|
3392
|
+
with cols[1]:
|
|
3393
|
+
settings[f"{prefix}_base_url"] = text_setting("Base URL", f"{prefix}_base_url")
|
|
3394
|
+
with cols[2]:
|
|
3395
|
+
settings[f"{prefix}_model_name"] = text_setting("Model", f"{prefix}_model_name")
|
|
3396
|
+
|
|
3397
|
+
with st.expander("Voz, TTS e música — Azure Speech, restantes serviços e Suno", expanded=True):
|
|
3398
|
+
cols = st.columns(2)
|
|
3352
3399
|
with cols[0]:
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3400
|
+
azure_speech_key = text_setting("Azure Speech key", "azure_speech_key", secret=True)
|
|
3401
|
+
azure_speech_region = text_setting("Azure Speech region", "azure_speech_region")
|
|
3402
|
+
siliconflow_tts_api_key = text_setting("SiliconFlow TTS API key", "siliconflow_tts_api_key", secret=True)
|
|
3403
|
+
minimax_tts_api_key = text_setting("MiniMax TTS API key", "minimax_tts_api_key", secret=True)
|
|
3404
|
+
minimax_tts_base_url = text_setting("MiniMax TTS Base URL", "minimax_tts_base_url")
|
|
3405
|
+
minimax_tts_model_id = text_setting("MiniMax TTS model", "minimax_tts_model_id")
|
|
3406
|
+
minimax_tts_voice_id = text_setting("MiniMax TTS voice ID", "minimax_tts_voice_id")
|
|
3357
3407
|
with cols[1]:
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
st.
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
with st.expander("Publicação através do Upload-Post"):
|
|
3407
|
-
upload_post_enabled = st.checkbox("Activar Upload-Post", bool(settings.get("upload_post_enabled", False)))
|
|
3408
|
-
upload_post_api_key = text_setting("Upload-Post API key", "upload_post_api_key", secret=True)
|
|
3409
|
-
upload_post_username = text_setting("Upload-Post username", "upload_post_username")
|
|
3410
|
-
upload_post_platforms = text_setting("Plataformas Upload-Post", "upload_post_platforms")
|
|
3411
|
-
upload_post_auto_upload = st.checkbox("Publicar automaticamente após gerar", bool(settings.get("upload_post_auto_upload", False)))
|
|
3412
|
-
|
|
3413
|
-
with st.expander("Postiz — API key, integração e MCP", expanded=True):
|
|
3414
|
-
st.caption("O Thunderbolt é o cliente. A API key é enviada exclusivamente ao servidor Postiz configurado; não é colocada em URLs, logs ou repositório.")
|
|
3415
|
-
postiz_enabled = st.checkbox("Activar Postiz como fallback final", bool(settings.get("postiz_enabled", False)))
|
|
3416
|
-
postiz_mode = st.selectbox("Modo de ligação", ["api", "mcp"], index=0 if settings.get("postiz_mode", "api") != "mcp" else 1, help="API é o modo determinístico de upload. MCP fica disponível para uma ligação compatível com Streamable HTTP.")
|
|
3417
|
-
postiz_cols = st.columns(2)
|
|
3418
|
-
with postiz_cols[0]:
|
|
3419
|
-
postiz_api_key = text_setting("Postiz API key", "postiz_api_key", secret=True, help_text="API key criada nas definições do Postiz. A API HTTP usa o valor bruto no cabeçalho Authorization.")
|
|
3420
|
-
postiz_base_url = text_setting("Postiz Public API Base URL", "postiz_base_url", help_text="Cloud: https://api.postiz.com/public/v1 · Self-hosted: https://seu-servidor/api/public/v1")
|
|
3421
|
-
postiz_integration_id = text_setting("Postiz integração padrão", "postiz_integration_id", help_text="ID do canal/integração devolvido por GET /integrations.")
|
|
3422
|
-
with postiz_cols[1]:
|
|
3423
|
-
postiz_mcp_url = text_setting("Postiz MCP URL", "postiz_mcp_url", help_text="Cloud: https://api.postiz.com/mcp · o cliente acrescenta a API key conforme o modo escolhido.")
|
|
3424
|
-
postiz_auto_publish = st.checkbox("Permitir publicação imediata no Postiz", bool(settings.get("postiz_auto_publish", False)))
|
|
3425
|
-
st.caption("No Upload, a aba Postiz permite carregar as integrações e enviar vídeos manualmente. No fluxo recomendado, Postiz só é tentado depois da API Oficial e do Upload directo.")
|
|
3426
|
-
|
|
3427
|
-
if refresh_openai_models:
|
|
3428
|
-
try:
|
|
3429
|
-
discovered_models = fetch_openai_compatible_models(openai_api_key, openai_base_url)
|
|
3430
|
-
st.session_state["openai_model_catalog"] = {"key": catalog_key, "models": discovered_models, "error": ""}
|
|
3431
|
-
st.success(f"{len(discovered_models)} modelo(s) carregado(s) do endpoint OpenAI-compatible.")
|
|
3432
|
-
st.rerun()
|
|
3433
|
-
except ModelDiscoveryError as exc:
|
|
3434
|
-
st.session_state["openai_model_catalog"] = {"key": catalog_key, "models": [], "error": str(exc)}
|
|
3435
|
-
st.rerun()
|
|
3408
|
+
elevenlabs_api_key = text_setting("ElevenLabs API key", "elevenlabs_api_key", secret=True)
|
|
3409
|
+
elevenlabs_model_id = text_setting("ElevenLabs model", "elevenlabs_model_id")
|
|
3410
|
+
chatterbox_base_url = text_setting("Chatterbox Base URL", "chatterbox_base_url")
|
|
3411
|
+
chatterbox_api_key = text_setting("Chatterbox API key", "chatterbox_api_key", secret=True)
|
|
3412
|
+
chatterbox_model_id = text_setting("Chatterbox model", "chatterbox_model_id")
|
|
3413
|
+
sonilo_api_key = text_setting("Sonilo API key", "sonilo_api_key", secret=True)
|
|
3414
|
+
sonilo_base_url = text_setting("Sonilo Base URL", "sonilo_base_url")
|
|
3415
|
+
st.markdown("**Suno — agente musical opcional**")
|
|
3416
|
+
suno_api_key = text_setting("Suno API key", "suno_api_key", secret=True)
|
|
3417
|
+
suno_api_base_url = text_setting("Suno API Base URL", "suno_api_base_url", help_text="Use o endpoint compatível fornecido pelo seu acesso Suno; não é inventado pelo Thunderbolt.")
|
|
3418
|
+
suno_api_endpoint = text_setting("Suno API endpoint", "suno_api_endpoint", help_text="Ex.: /api/generate")
|
|
3419
|
+
|
|
3420
|
+
with st.expander("TikTok for Developers — Client ID e Client Secret", expanded=True):
|
|
3421
|
+
st.caption("Apenas as credenciais da aplicação ficam nesta UI. Redirect URI, scopes, autorização e tokens são geridos no TikTok for Developers Playground.")
|
|
3422
|
+
tiktok_client_key = text_setting("TikTok Client ID", "tiktok_client_key", secret=True)
|
|
3423
|
+
tiktok_client_secret = text_setting("TikTok Client Secret", "tiktok_client_secret", secret=True)
|
|
3424
|
+
|
|
3425
|
+
with st.expander("Publicação através do Upload-Post"):
|
|
3426
|
+
upload_post_enabled = st.checkbox("Activar Upload-Post", bool(settings.get("upload_post_enabled", False)))
|
|
3427
|
+
upload_post_api_key = text_setting("Upload-Post API key", "upload_post_api_key", secret=True)
|
|
3428
|
+
upload_post_username = text_setting("Upload-Post username", "upload_post_username")
|
|
3429
|
+
upload_post_platforms = text_setting("Plataformas Upload-Post", "upload_post_platforms")
|
|
3430
|
+
upload_post_auto_upload = st.checkbox("Publicar automaticamente após gerar", bool(settings.get("upload_post_auto_upload", False)))
|
|
3431
|
+
|
|
3432
|
+
with st.expander("Postiz — API key, integração e MCP", expanded=True):
|
|
3433
|
+
st.caption("O Thunderbolt é o cliente. A API key é enviada exclusivamente ao servidor Postiz configurado; não é colocada em URLs, logs ou repositório.")
|
|
3434
|
+
postiz_enabled = st.checkbox("Activar Postiz como fallback final", bool(settings.get("postiz_enabled", False)))
|
|
3435
|
+
postiz_mode = st.selectbox("Modo de ligação", ["api", "mcp"], index=0 if settings.get("postiz_mode", "api") != "mcp" else 1, help="API é o modo determinístico de upload. MCP fica disponível para uma ligação compatível com Streamable HTTP.")
|
|
3436
|
+
postiz_cols = st.columns(2)
|
|
3437
|
+
with postiz_cols[0]:
|
|
3438
|
+
postiz_api_key = text_setting("Postiz API key", "postiz_api_key", secret=True, help_text="API key criada nas definições do Postiz. A API HTTP usa o valor bruto no cabeçalho Authorization.")
|
|
3439
|
+
postiz_base_url = text_setting("Postiz Public API Base URL", "postiz_base_url", help_text="Cloud: https://api.postiz.com/public/v1 · Self-hosted: https://seu-servidor/api/public/v1")
|
|
3440
|
+
postiz_integration_id = text_setting("Postiz integração padrão", "postiz_integration_id", help_text="ID do canal/integração devolvido por GET /integrations.")
|
|
3441
|
+
with postiz_cols[1]:
|
|
3442
|
+
postiz_mcp_url = text_setting("Postiz MCP URL", "postiz_mcp_url", help_text="Cloud: https://api.postiz.com/mcp · o cliente acrescenta a API key conforme o modo escolhido.")
|
|
3443
|
+
postiz_auto_publish = st.checkbox("Permitir publicação imediata no Postiz", bool(settings.get("postiz_auto_publish", False)))
|
|
3444
|
+
st.caption("No Upload, a aba Postiz permite carregar as integrações e enviar vídeos manualmente. No fluxo recomendado, Postiz só é tentado depois da API Oficial e do Upload directo.")
|
|
3445
|
+
|
|
3446
|
+
if refresh_openai_models:
|
|
3447
|
+
try:
|
|
3448
|
+
discovered_models = fetch_openai_compatible_models(openai_api_key, openai_base_url)
|
|
3449
|
+
st.session_state["openai_model_catalog"] = {"key": catalog_key, "models": discovered_models, "error": ""}
|
|
3450
|
+
st.success(f"{len(discovered_models)} modelo(s) carregado(s) do endpoint OpenAI-compatible.")
|
|
3451
|
+
st.rerun()
|
|
3452
|
+
except ModelDiscoveryError as exc:
|
|
3453
|
+
st.session_state["openai_model_catalog"] = {"key": catalog_key, "models": [], "error": str(exc)}
|
|
3454
|
+
st.rerun()
|
|
3436
3455
|
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
"
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
st.success("Configurações guardadas localmente. Indique uma pasta válida do motor de vídeo para sincronizar config.toml.")
|
|
3471
|
-
except Exception as exc:
|
|
3472
|
-
st.warning(f"Configurações locais guardadas, mas não foi possível sincronizar config.toml: {exc}")
|
|
3456
|
+
save_all_settings = st.form_submit_button("Guardar configurações do Thunderbolt", type="primary")
|
|
3457
|
+
if save_all_settings:
|
|
3458
|
+
settings.update({
|
|
3459
|
+
"port": port, "moneyprinter_path": moneyprinter_path,
|
|
3460
|
+
"kaggle_username": kaggle_username.strip(), "kaggle_api_key": kaggle_api_key.strip(), "kaggle_kernel_slug": kaggle_kernel_slug.strip() or "thunderbolt-niche-finder",
|
|
3461
|
+
"apify_api_token": apify_api_token.strip(), "apify_actor_id": apify_actor_id.strip() or DEFAULT_ACTOR_ID, "apify_poll_interval_seconds": int(apify_poll_interval), "apify_run_timeout_seconds": int(apify_run_timeout),
|
|
3462
|
+
"llm_provider": llm_provider, "openai_api_key": openai_api_key, "openai_base_url": openai_base_url, "openai_model_name": openai_model_name,
|
|
3463
|
+
"gemini_image_api_key": gemini_image_api_key, "gemini_image_model": gemini_image_model, "gemini_image_aspect_ratio": gemini_image_aspect_ratio, "gemini_image_size": gemini_image_size,
|
|
3464
|
+
"azure_speech_key": azure_speech_key, "azure_speech_region": azure_speech_region,
|
|
3465
|
+
"siliconflow_tts_api_key": siliconflow_tts_api_key, "minimax_tts_api_key": minimax_tts_api_key,
|
|
3466
|
+
"minimax_tts_base_url": minimax_tts_base_url, "minimax_tts_model_id": minimax_tts_model_id, "minimax_tts_voice_id": minimax_tts_voice_id,
|
|
3467
|
+
"elevenlabs_api_key": elevenlabs_api_key, "elevenlabs_model_id": elevenlabs_model_id,
|
|
3468
|
+
"chatterbox_base_url": chatterbox_base_url, "chatterbox_api_key": chatterbox_api_key, "chatterbox_model_id": chatterbox_model_id,
|
|
3469
|
+
"sonilo_api_key": sonilo_api_key, "sonilo_base_url": sonilo_base_url, "suno_api_key": suno_api_key, "suno_api_base_url": suno_api_base_url, "suno_api_endpoint": suno_api_endpoint,
|
|
3470
|
+
"tiktok_client_key": tiktok_client_key, "tiktok_client_secret": tiktok_client_secret,
|
|
3471
|
+
"upload_post_enabled": upload_post_enabled, "upload_post_api_key": upload_post_api_key,
|
|
3472
|
+
"upload_post_username": upload_post_username, "upload_post_platforms": upload_post_platforms,
|
|
3473
|
+
"upload_post_auto_upload": upload_post_auto_upload,
|
|
3474
|
+
"postiz_enabled": postiz_enabled, "postiz_api_key": postiz_api_key, "postiz_base_url": postiz_base_url.strip() or "https://api.postiz.com/public/v1",
|
|
3475
|
+
"postiz_mcp_url": postiz_mcp_url.strip() or "https://api.postiz.com/mcp", "postiz_mode": postiz_mode,
|
|
3476
|
+
"postiz_integration_id": postiz_integration_id.strip(), "postiz_auto_publish": bool(postiz_auto_publish),
|
|
3477
|
+
})
|
|
3478
|
+
write_json("settings.json", settings)
|
|
3479
|
+
try:
|
|
3480
|
+
synced = sync_moneyprinter_config(settings, moneyprinter_path)
|
|
3481
|
+
if synced:
|
|
3482
|
+
st.success(f"Configurações guardadas e sincronizadas com {synced}")
|
|
3483
|
+
else:
|
|
3484
|
+
st.success("Configurações guardadas localmente. Indique uma pasta válida do motor de vídeo para sincronizar config.toml.")
|
|
3485
|
+
except Exception as exc:
|
|
3486
|
+
st.warning(f"Configurações locais guardadas, mas não foi possível sincronizar config.toml: {exc}")
|
|
3487
|
+
with material_sources_tab:
|
|
3488
|
+
render_material_source_api_keys(settings)
|
|
3473
3489
|
|
|
3474
3490
|
with voice_test_tab:
|
|
3475
3491
|
st.subheader("Teste de vozes")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
MATERIAL_SOURCE_CATALOG: tuple[dict[str, str], ...] = (
|
|
7
|
+
{"code": "pexels", "label": "Pexels", "description": "Banco de vídeos e imagens para materiais da pipeline.", "legacy_key": "pexels_api_keys"},
|
|
8
|
+
{"code": "pixabay", "label": "Pixabay", "description": "Banco de vídeos e imagens para materiais da pipeline.", "legacy_key": "pixabay_api_keys"},
|
|
9
|
+
{"code": "coverr", "label": "Coverr", "description": "Fonte de vídeos de stock com API própria.", "legacy_key": "coverr_api_keys"},
|
|
10
|
+
{"code": "wavespeed", "label": "WaveSpeed AI", "description": "Geração de clips por IA; requer um serviço configurado.", "legacy_key": "wavespeed_api_keys"},
|
|
11
|
+
{"code": "loomloom", "label": "LoomLoom", "description": "Fonte paga de materiais; requer confirmação no fluxo de criação.", "legacy_key": "loomloom_api_keys"},
|
|
12
|
+
{"code": "twelvelabs", "label": "TwelveLabs", "description": "Ranking e análise semântica opcional dos materiais.", "legacy_key": "twelvelabs_api_keys"},
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _as_key_list(value: Any) -> list[str]:
|
|
17
|
+
if isinstance(value, list):
|
|
18
|
+
values = value
|
|
19
|
+
else:
|
|
20
|
+
values = str(value or "").replace("\n", ",").split(",")
|
|
21
|
+
result: list[str] = []
|
|
22
|
+
seen: set[str] = set()
|
|
23
|
+
for item in values:
|
|
24
|
+
key = str(item or "").strip()
|
|
25
|
+
if key and key not in seen:
|
|
26
|
+
result.append(key)
|
|
27
|
+
seen.add(key)
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def material_source_catalog() -> list[dict[str, str]]:
|
|
32
|
+
return [dict(item) for item in MATERIAL_SOURCE_CATALOG]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def material_api_keys(settings: dict[str, Any], source: str) -> list[str]:
|
|
36
|
+
code = str(source or "").strip().lower()
|
|
37
|
+
saved = settings.get("material_api_keys", {})
|
|
38
|
+
if isinstance(saved, dict) and code in saved:
|
|
39
|
+
return _as_key_list(saved.get(code))
|
|
40
|
+
legacy_key = next((item["legacy_key"] for item in MATERIAL_SOURCE_CATALOG if item["code"] == code), f"{code}_api_keys")
|
|
41
|
+
return _as_key_list(settings.get(legacy_key, ""))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def all_material_api_keys(settings: dict[str, Any]) -> dict[str, list[str]]:
|
|
45
|
+
return {item["code"]: material_api_keys(settings, item["code"]) for item in MATERIAL_SOURCE_CATALOG}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def update_material_api_keys(settings: dict[str, Any], source: str, keys: list[str]) -> dict[str, Any]:
|
|
49
|
+
code = str(source or "").strip().lower()
|
|
50
|
+
if code not in {item["code"] for item in MATERIAL_SOURCE_CATALOG}:
|
|
51
|
+
raise ValueError("Fonte de materiais inválida.")
|
|
52
|
+
cleaned = _as_key_list(keys)
|
|
53
|
+
mapping = all_material_api_keys(settings)
|
|
54
|
+
mapping[code] = cleaned
|
|
55
|
+
settings["material_api_keys"] = mapping
|
|
56
|
+
source_entry = next(item for item in MATERIAL_SOURCE_CATALOG if item["code"] == code)
|
|
57
|
+
settings[source_entry["legacy_key"]] = cleaned
|
|
58
|
+
return settings
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def selected_material_source(settings: dict[str, Any]) -> str:
|
|
62
|
+
source = str(settings.get("video_source") or "pexels").strip().lower()
|
|
63
|
+
valid = {item["code"] for item in MATERIAL_SOURCE_CATALOG} | {"local"}
|
|
64
|
+
return source if source in valid else "pexels"
|
package/hermes_ui/storage.py
CHANGED
|
@@ -197,9 +197,12 @@ DEFAULTS: dict[str, Any] = {
|
|
|
197
197
|
"endpoint": "",
|
|
198
198
|
"proxy_http": "",
|
|
199
199
|
"proxy_https": "",
|
|
200
|
+
"material_api_keys": {},
|
|
200
201
|
"pexels_api_keys": "",
|
|
201
202
|
"pixabay_api_keys": "",
|
|
202
203
|
"coverr_api_keys": "",
|
|
204
|
+
"wavespeed_api_keys": "",
|
|
205
|
+
"loomloom_api_keys": "",
|
|
203
206
|
"twelvelabs_api_keys": "",
|
|
204
207
|
"sonilo_api_key": "",
|
|
205
208
|
"subtitle_provider": "edge",
|
|
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
from hermes_ui.material_sources import all_material_api_keys, selected_material_source
|
|
7
|
+
|
|
6
8
|
try:
|
|
7
9
|
import toml
|
|
8
10
|
except ImportError: # pragma: no cover - installation fallback
|
|
@@ -112,9 +114,13 @@ def build_moneyprinter_config(settings: dict[str, Any], existing: dict[str, Any]
|
|
|
112
114
|
for settings_key, config_key in app_map.items():
|
|
113
115
|
if settings_key in settings:
|
|
114
116
|
app[config_key] = settings[settings_key]
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
# A UI can store the canonical mapping while older installations still
|
|
118
|
+
# expose one legacy field per source. Export every supported source as a
|
|
119
|
+
# TOML array so MoneyPrinterTurbo can rotate keys without CSV parsing.
|
|
120
|
+
canonical_material_keys = all_material_api_keys(settings)
|
|
121
|
+
for source, keys in canonical_material_keys.items():
|
|
122
|
+
app[f"{source}_api_keys"] = _list_value(keys)
|
|
123
|
+
app["video_source"] = selected_material_source(settings)
|
|
118
124
|
config["app"] = app
|
|
119
125
|
|
|
120
126
|
whisper["model_size"] = settings.get("whisper_model_size", whisper.get("model_size", "large-v3"))
|
package/package.json
CHANGED