@danhachuel/thunderbolt 0.3.65 → 0.3.67
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 +16 -6
- package/README.md +11 -3
- package/app/main.py +104 -16
- package/hermes_ui/api_key_tests.py +3 -1
- package/hermes_ui/languages.py +4 -1
- package/hermes_ui/media_generation.py +40 -3
- package/hermes_ui/media_providers.py +22 -1
- package/hermes_ui/pipeline_worker.py +7 -4
- package/hermes_ui/storage.py +1 -0
- package/integrations/platforms.py +12 -2
- package/package.json +1 -1
package/MANUAL-INSTALACAO.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Este manual descreve a instalação local da UI Thunderbolt, baseada no MoneyPrinterTurbo, utilizando o pacote npm `@danhachuel/thunderbolt`. O fluxo recomendado instala automaticamente o ambiente Python, as dependências da aplicação, as dependências do MoneyPrinterTurbo, o Streamlit e o suporte FFmpeg através de `imageio-ffmpeg`.
|
|
4
4
|
|
|
5
|
-
> **Versão deste manual:** 0.3.
|
|
5
|
+
> **Versão deste manual:** 0.3.67
|
|
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)
|
|
@@ -128,13 +128,13 @@ Execute:
|
|
|
128
128
|
Windows PowerShell ou MobaXterm:
|
|
129
129
|
|
|
130
130
|
```powershell
|
|
131
|
-
npx.cmd --yes --prefer-online @danhachuel/thunderbolt@0.3.
|
|
131
|
+
npx.cmd --yes --prefer-online @danhachuel/thunderbolt@0.3.67 install
|
|
132
132
|
```
|
|
133
133
|
|
|
134
134
|
Linux/macOS:
|
|
135
135
|
|
|
136
136
|
```bash
|
|
137
|
-
npx --yes --prefer-online @danhachuel/thunderbolt@0.3.
|
|
137
|
+
npx --yes --prefer-online @danhachuel/thunderbolt@0.3.67 install
|
|
138
138
|
```
|
|
139
139
|
|
|
140
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.
|
|
@@ -142,7 +142,7 @@ A instalação normal é **segura para actualizações**: preserva `storage`, Bl
|
|
|
142
142
|
Se quiser apagar absolutamente tudo de forma intencional, use o comando destrutivo separado:
|
|
143
143
|
|
|
144
144
|
```powershell
|
|
145
|
-
npx.cmd --yes --prefer-online @danhachuel/thunderbolt@0.3.
|
|
145
|
+
npx.cmd --yes --prefer-online @danhachuel/thunderbolt@0.3.67 install --purge-data
|
|
146
146
|
```
|
|
147
147
|
|
|
148
148
|
O parâmetro `--purge-data` apaga Blueprints, Brandings, configurações, storage e artefactos locais. Não o use numa actualização normal.
|
|
@@ -395,6 +395,14 @@ O expander **Imagem e Video** substitui o antigo bloco isolado da Nano Banana. O
|
|
|
395
395
|
|
|
396
396
|
A área **Voz, TTS e música — Azure Speech, restantes serviços e Suno** está dividida em cartões independentes para **Azure Speech**, **ElevenLabs**, **SiliconFlow**, **MiniMax TTS**, **Chatterbox**, **Sonilo** e **Suno**. Cada cartão agrupa apenas as credenciais, parâmetros e diagnóstico do serviço correspondente. O Suno aparece num cartão próprio porque é uma integração de criação musical, enquanto os restantes providers são serviços de voz/TTS.
|
|
397
397
|
|
|
398
|
+
Na subaba **Imagem e Video**, a lista **Provider de media** inclui **FAL AI, KIE AI, Agnes AI, Nano Banana, Replicate AI, Pollinations.ai, Hugging Face Inference API, InferencePort Proxy e HeyGen** para a rota Full IA. O cartão **HeyGen** pede a API key, o **Avatar ID** e, opcionalmente, o **Voice ID**; a opção **Provider activo**, **Pool Vídeo**, **Prioridade** e **Testar Chamada API** funcionam como nos restantes cartões. O teste HeyGen usa apenas `GET /v3/users/me` e não consome uma geração. Nano Banana e Hugging Face podem permanecer no catálogo sem serem seleccionados para vídeo quando o cartão não declara capacidade de vídeo.
|
|
399
|
+
|
|
400
|
+
### API Tiktok — várias aplicações
|
|
401
|
+
|
|
402
|
+
Em **Configuração API > API Tiktok**, situada entre **Contas Google** e **Fontes de Materiais**, cada aplicação TikTok é configurada num card independente. O card contém exclusivamente **TikTok Client ID** e **TikTok Client Secret** e apresenta os botões **Testar chamada API**, **Guardar card** e **Apagar card**. Use **Adicionar nova API** no final da lista para criar outra aplicação sem substituir as anteriores.
|
|
403
|
+
|
|
404
|
+
O botão de teste é read-only. Sem um token OAuth TikTok já autorizado, o Thunderbolt informa que é necessário concluir o Playground/autorização do TikTok for Developers, sem inventar um resultado nem enviar credenciais. O token e a autorização não são campos dos cards. Instalações que ainda tenham `tiktok_client_key` e `tiktok_client_secret` são migradas automaticamente para o primeiro card; o adapter usa o primeiro card completo e mantém fallback para os campos antigos.
|
|
405
|
+
|
|
398
406
|
Na criação de vídeo, abra **Configurações de áudio** e escolha **Upload** em **Modo de narração**. Use **Ficheiro de narração** para seleccionar o áudio, clique em **Guardar áudio de narração** e confirme a pré-visualização. O Thunderbolt valida que o ficheiro existe antes de criar a tarefa e encaminha o caminho ao MoneyPrinterTurbo com `--custom-audio-file`; este argumento é necessário porque o motor não persiste automaticamente o caminho carregado. São aceites ficheiros `.mp3`, `.wav`, `.m4a`, `.aac`, `.flac` e `.ogg`, guardados no storage local em `voiceovers`.
|
|
399
407
|
|
|
400
408
|
O selector **Voiceover Service** oferece **Azure Speech SDK V2** e **Azure TTS V1**. Quando existe **Azure Speech key + região**, a opção V2 é preferida e o worker marca internamente a voz com `-V2`, activando o SDK Azure Speech e evitando o stream `edge_tts`. Tarefas antigas que ainda tenham `Azure TTS V1` guardado também são migradas para V2 quando essas credenciais estão configuradas. Sem credenciais Azure, o V1 continua disponível como fallback sem key; nesse caso o timeout interno do stream passa a 90 segundos para tolerar scripts longos e redes lentas. Um erro de TTS identifica explicitamente **Azure Speech / edge_tts API**, a etapa **Narração TTS** e os providers envolvidos.
|
|
@@ -526,7 +534,9 @@ Após iniciar a aplicação, valide o seguinte percurso:
|
|
|
526
534
|
|
|
527
535
|
## Configuração API — fontes de materiais
|
|
528
536
|
|
|
529
|
-
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.
|
|
537
|
+
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.
|
|
538
|
+
|
|
539
|
+
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.
|
|
530
540
|
|
|
531
541
|
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.
|
|
532
542
|
|
|
@@ -558,7 +568,7 @@ Ao abrir a página, o Thunderbolt não prepara dados públicos, não descarrega
|
|
|
558
568
|
|
|
559
569
|
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.
|
|
560
570
|
|
|
561
|
-
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.
|
|
571
|
+
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.67 install`; o instalador detecta e reutiliza o que já estiver válido.
|
|
562
572
|
|
|
563
573
|
### Niche Finder Apify
|
|
564
574
|
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ A primeira versão implementa a camada UI independente com:
|
|
|
25
25
|
| 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 |
|
|
26
26
|
| 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 |
|
|
27
27
|
| MCP | Catálogo local opcional de Short Video Maker, AutoVio, OpenMontage e OpenCut, com portas editáveis e activação |
|
|
28
|
-
| Configurações | **Contas Google**, **Configuração API** e **Notificações**; Notificações contém as subabas **Geral** e **Telegram Gateway**, com envio das mesmas operações locais para um Chat ID; 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` global para todas as contas, providers, TTS, Nano Banana, Postiz, TikTok e Upload-Post |
|
|
28
|
+
| Configurações | **Contas Google**, **Configuração API** e **Notificações**; Notificações contém as subabas **Geral** e **Telegram Gateway**, com envio das mesmas operações locais para um Chat ID; API Keys divididas entre **Serviços e modelos**, **API Tiktok** e **Fontes de materiais**, com várias chaves independentes por fonte, além de contas Google/YouTube por cartão, `INNERTUBE_API_KEY` global para todas as contas, providers, TTS, Nano Banana, Postiz, TikTok e Upload-Post |
|
|
29
29
|
| Launcher | Execução via `npx`, instalação assistida, diagnóstico e preparação para distribuição |
|
|
30
30
|
|
|
31
31
|
## AI Influencers — personagens e geração de conteúdo
|
|
@@ -42,10 +42,18 @@ A migração é baseada no workflow público [AI Agents A-Z — episódio 35](ht
|
|
|
42
42
|
|
|
43
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
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
|
|
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 o pool de vídeo configurável com **FAL AI, KIE AI, Agnes AI, Nano Banana, Replicate AI, Pollinations.ai, Hugging Face Inference API, InferencePort Proxy e HeyGen**, respeitando apenas cartões activos que declarem capacidade 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
46
|
|
|
47
47
|
Quando uma etapa falha, a tarefa, a notificação e a página **Configurações > Logs** guardam e mostram sempre a coluna **API/Provider**, o serviço, a rota e, quando aplicável, os campos de configuração em falta. No caso do MoneyPrinterTurbo, os marcadores `LLM_PROVIDER`, `MISSING` e `INVALID` são convertidos em attribution legível; por exemplo, um erro pode indicar simultaneamente **OpenAI / NVIDIA NIM API** e **Pexels API**, em vez de apresentar apenas a mensagem genérica de credenciais adicionais. Os timeouts `azure_tts_v1`/`edge_tts` são identificados como **Azure Speech / edge_tts API**. Quando há Azure Speech key e região, o worker encaminha a voz para o SDK Azure Speech V2; sem essas credenciais, o fallback edge_tts usa um timeout interno de 90 segundos. O worker invoca o helper com `--` antes das flags MoneyPrinterTurbo, porque `mpt_agent.py` reserva esses argumentos para a CLI filha. Registos históricos sem metadata são identificados explicitamente como anteriores à attribution estruturada.
|
|
48
48
|
|
|
49
|
+
Na subaba **Configuração API > API Keys > Imagem e Video**, o selector **Provider de media** inclui a mesma lista do pool Full IA. O cartão **HeyGen** usa a API V3, apresenta os campos **Avatar ID** e **Voice ID**, valida a credencial com uma chamada read-only e participa no failover de vídeo quando estiver activo e configurado. Nano Banana e Hugging Face permanecem disponíveis no catálogo; a participação efectiva no pool de vídeo depende da capacidade declarada pelo cartão, para não encaminhar vídeo para um endpoint que apenas suporte imagem.
|
|
50
|
+
|
|
51
|
+
## API Tiktok — múltiplas aplicações TikTok
|
|
52
|
+
|
|
53
|
+
A subaba **Configuração API > API Tiktok** fica entre **Contas Google** e **Fontes de Materiais**. Cada aplicação TikTok é apresentada num card separado e contém apenas **TikTok Client ID** e **TikTok Client Secret**, ambos guardados no storage local. O botão **Adicionar nova API** cria cards adicionais; cada card dispõe de **Guardar card**, **Testar chamada API** e **Apagar card**. A autorização OAuth, os scopes e os tokens de publicação continuam fora desta subaba, no fluxo próprio do TikTok for Developers.
|
|
54
|
+
|
|
55
|
+
As credenciais antigas dos campos únicos `tiktok_client_key` e `tiktok_client_secret` são migradas automaticamente para o primeiro card sem serem eliminadas. Para compatibilidade com integrações existentes, o adapter TikTok utiliza o primeiro card completo e mantém fallback para os campos legados; a UI não altera os campos pedidos nem mistura Client ID/Secret com tokens, redirect URI ou scopes.
|
|
56
|
+
|
|
49
57
|
## Upload-Post — publicação para múltiplas plataformas
|
|
50
58
|
|
|
51
59
|
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**.
|
|
@@ -87,7 +95,7 @@ No topo da área principal da aplicação existe o menu nativo de idioma no padr
|
|
|
87
95
|
|
|
88
96
|
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.
|
|
89
97
|
|
|
90
|
-
## Navegação da UI 0.3.
|
|
98
|
+
## Navegação da UI 0.3.66
|
|
91
99
|
|
|
92
100
|
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.
|
|
93
101
|
**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
|
@@ -40,7 +40,7 @@ from hermes_ui.mcp import detect_local_service, install_skill_locally, load_inte
|
|
|
40
40
|
from hermes_ui.mcp_server import server_status, start_server, stop_server
|
|
41
41
|
from hermes_ui.material_sources import apply_material_source_cards_to_settings, ensure_material_source_cards, material_source_catalog, material_source_definition, new_material_card, normalize_material_card, selected_material_source
|
|
42
42
|
from hermes_ui.llm_providers import LLM_CARDS_KEY, LLM_PROVIDER_CATALOG, apply_llm_cards_to_settings, ensure_llm_provider_cards, new_llm_card, normalize_llm_card, provider_definition, test_llm_provider_card, stamp_test_result
|
|
43
|
-
from hermes_ui.media_providers import MEDIA_CARDS_KEY, MEDIA_IMAGE_ACTIVE_CARD_KEY, MEDIA_VIDEO_ACTIVE_CARD_KEY, apply_media_provider_cards_to_settings, ensure_media_provider_cards, media_cards_for_pool, media_provider_catalog, media_provider_definition, new_media_card, normalize_media_card
|
|
43
|
+
from hermes_ui.media_providers import FULL_IA_VIDEO_PROVIDER_CODES, MEDIA_CARDS_KEY, MEDIA_IMAGE_ACTIVE_CARD_KEY, MEDIA_VIDEO_ACTIVE_CARD_KEY, apply_media_provider_cards_to_settings, ensure_media_provider_cards, media_cards_for_pool, media_provider_catalog, media_provider_definition, new_media_card, normalize_media_card
|
|
44
44
|
from hermes_ui.music import list_music_files, materialize_suno_audio, request_suno_generation, store_music_file, store_voiceover_file
|
|
45
45
|
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
|
|
46
46
|
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
|
|
@@ -4660,6 +4660,103 @@ def render_upload_conventional():
|
|
|
4660
4660
|
st.button("Preparar Facebook Pages", key=f"upload_facebook_{task['id']}", disabled=True, help="UI preparada; publicação Facebook Pages ainda não está activa.")
|
|
4661
4661
|
|
|
4662
4662
|
|
|
4663
|
+
def _normalise_tiktok_api_cards(settings: dict[str, Any]) -> tuple[list[dict[str, str]], bool]:
|
|
4664
|
+
raw_cards = settings.get("tiktok_api_cards")
|
|
4665
|
+
cards: list[dict[str, str]] = []
|
|
4666
|
+
changed = not isinstance(raw_cards, list)
|
|
4667
|
+
if isinstance(raw_cards, list):
|
|
4668
|
+
for index, raw_card in enumerate(raw_cards):
|
|
4669
|
+
if not isinstance(raw_card, dict):
|
|
4670
|
+
changed = True
|
|
4671
|
+
continue
|
|
4672
|
+
card_id = str(raw_card.get("id") or f"tiktok-api-{index + 1}").strip()
|
|
4673
|
+
client_id = str(raw_card.get("client_id") or raw_card.get("client_key") or "").strip()
|
|
4674
|
+
client_secret = str(raw_card.get("client_secret") or "").strip()
|
|
4675
|
+
normalised = {"id": card_id, "client_id": client_id, "client_secret": client_secret}
|
|
4676
|
+
cards.append(normalised)
|
|
4677
|
+
if normalised != {key: str(raw_card.get(key) or "").strip() for key in ("id", "client_id", "client_secret")}:
|
|
4678
|
+
changed = True
|
|
4679
|
+
if not cards:
|
|
4680
|
+
legacy_client_id = str(settings.get("tiktok_client_key") or "").strip()
|
|
4681
|
+
legacy_client_secret = str(settings.get("tiktok_client_secret") or "").strip()
|
|
4682
|
+
if legacy_client_id or legacy_client_secret:
|
|
4683
|
+
cards = [{"id": "tiktok-api-1", "client_id": legacy_client_id, "client_secret": legacy_client_secret}]
|
|
4684
|
+
changed = True
|
|
4685
|
+
if changed and cards:
|
|
4686
|
+
settings["tiktok_api_cards"] = cards
|
|
4687
|
+
write_json("settings.json", settings)
|
|
4688
|
+
return cards, changed
|
|
4689
|
+
|
|
4690
|
+
|
|
4691
|
+
def _persist_tiktok_api_cards(settings: dict[str, Any], cards: list[dict[str, str]]) -> None:
|
|
4692
|
+
clean_cards = [
|
|
4693
|
+
{
|
|
4694
|
+
"id": str(card.get("id") or f"tiktok-api-{index + 1}").strip(),
|
|
4695
|
+
"client_id": str(card.get("client_id") or "").strip(),
|
|
4696
|
+
"client_secret": str(card.get("client_secret") or "").strip(),
|
|
4697
|
+
}
|
|
4698
|
+
for index, card in enumerate(cards)
|
|
4699
|
+
]
|
|
4700
|
+
settings["tiktok_api_cards"] = clean_cards
|
|
4701
|
+
first = next((card for card in clean_cards if card["client_id"] and card["client_secret"]), clean_cards[0] if clean_cards else {"client_id": "", "client_secret": ""})
|
|
4702
|
+
settings["tiktok_client_key"] = first["client_id"]
|
|
4703
|
+
settings["tiktok_client_secret"] = first["client_secret"]
|
|
4704
|
+
write_json("settings.json", settings)
|
|
4705
|
+
|
|
4706
|
+
|
|
4707
|
+
def render_tiktok_api_cards(settings: dict[str, Any]) -> None:
|
|
4708
|
+
st.title("API Tiktok")
|
|
4709
|
+
st.caption("Configure várias aplicações TikTok em cards separados. Cada card mantém apenas o TikTok Client ID e o TikTok Client Secret; autorização OAuth, scopes e tokens continuam a ser geridos no TikTok for Developers Playground.")
|
|
4710
|
+
cards, _ = _normalise_tiktok_api_cards(settings)
|
|
4711
|
+
if not cards:
|
|
4712
|
+
st.info("Ainda não existe nenhuma API TikTok configurada. Use o botão abaixo para criar o primeiro card.")
|
|
4713
|
+
for index, card in enumerate(cards):
|
|
4714
|
+
card_id = str(card["id"])
|
|
4715
|
+
with st.container(border=True):
|
|
4716
|
+
header_cols = st.columns([3.2, 1.2])
|
|
4717
|
+
with header_cols[0]:
|
|
4718
|
+
st.subheader(f"API TikTok {index + 1}")
|
|
4719
|
+
with header_cols[1]:
|
|
4720
|
+
configured = bool(card["client_id"] and card["client_secret"])
|
|
4721
|
+
_api_status_badge("Configured" if configured else "Missing configuration", "ready" if configured else "missing")
|
|
4722
|
+
with st.form(f"tiktok_api_card_form_{card_id}"):
|
|
4723
|
+
client_id = st.text_input("TikTok Client ID", value=card["client_id"], key=f"tiktok_api_{card_id}_client_id")
|
|
4724
|
+
client_secret = st.text_input("TikTok Client Secret", value=card["client_secret"], type="password", key=f"tiktok_api_{card_id}_client_secret")
|
|
4725
|
+
action_cols = st.columns(3)
|
|
4726
|
+
with action_cols[0]:
|
|
4727
|
+
test_clicked = st.form_submit_button("Testar chamada API", use_container_width=True)
|
|
4728
|
+
with action_cols[1]:
|
|
4729
|
+
save_clicked = st.form_submit_button("Guardar card", type="primary", use_container_width=True)
|
|
4730
|
+
with action_cols[2]:
|
|
4731
|
+
delete_clicked = st.form_submit_button("Apagar card", use_container_width=True)
|
|
4732
|
+
edited = {"id": card_id, "client_id": client_id.strip(), "client_secret": client_secret.strip()}
|
|
4733
|
+
if test_clicked:
|
|
4734
|
+
cards[index] = edited
|
|
4735
|
+
_persist_tiktok_api_cards(settings, cards)
|
|
4736
|
+
result = test_tiktok_credentials(edited["client_id"], edited["client_secret"], settings.get("tiktok_access_token", ""))
|
|
4737
|
+
if result.get("status") == "success":
|
|
4738
|
+
st.success(result.get("message") or "Chamada TikTok concluída.")
|
|
4739
|
+
elif result.get("status") == "unsupported":
|
|
4740
|
+
st.warning(result.get("message") or "Conclua a autorização TikTok antes do teste.")
|
|
4741
|
+
else:
|
|
4742
|
+
st.error(result.get("message") or "A chamada TikTok falhou.")
|
|
4743
|
+
elif save_clicked:
|
|
4744
|
+
cards[index] = edited
|
|
4745
|
+
_persist_tiktok_api_cards(settings, cards)
|
|
4746
|
+
st.success("Card TikTok guardado.")
|
|
4747
|
+
st.rerun()
|
|
4748
|
+
elif delete_clicked:
|
|
4749
|
+
cards = [item for item in cards if str(item.get("id")) != card_id]
|
|
4750
|
+
_persist_tiktok_api_cards(settings, cards)
|
|
4751
|
+
st.success("Card TikTok apagado.")
|
|
4752
|
+
st.rerun()
|
|
4753
|
+
if st.button("Adicionar nova API", type="primary", use_container_width=True, key="add_tiktok_api_card"):
|
|
4754
|
+
cards.append({"id": f"tiktok-api-{uuid.uuid4().hex[:10]}", "client_id": "", "client_secret": ""})
|
|
4755
|
+
_persist_tiktok_api_cards(settings, cards)
|
|
4756
|
+
st.success("Novo card TikTok criado.")
|
|
4757
|
+
st.rerun()
|
|
4758
|
+
|
|
4759
|
+
|
|
4663
4760
|
def render_google_accounts():
|
|
4664
4761
|
st.title("Contas Google")
|
|
4665
4762
|
st.caption("Gestão das contas Google/YouTube, documentos de credenciais, canais em lote e credenciais globais do YouTube.")
|
|
@@ -5394,7 +5491,8 @@ def render_media_provider_cards(settings: dict[str, Any], *, embedded: bool = Fa
|
|
|
5394
5491
|
settings.update(migrated)
|
|
5395
5492
|
write_json("settings.json", settings)
|
|
5396
5493
|
with st.expander("Imagem e Video", expanded=False):
|
|
5397
|
-
|
|
5494
|
+
full_ia_labels = ", ".join(media_provider_definition(code).label for code in FULL_IA_VIDEO_PROVIDER_CODES)
|
|
5495
|
+
st.caption(f"Configure providers de imagem e vídeo em cartões independentes. O router usa apenas o pool correspondente e faz failover entre providers activos. Pool Full IA: {full_ia_labels}.")
|
|
5398
5496
|
image_cards = [card for card in cards if card.get("supports_image")]
|
|
5399
5497
|
video_cards = [card for card in cards if card.get("supports_video")]
|
|
5400
5498
|
selector_cols = st.columns(3)
|
|
@@ -5439,7 +5537,7 @@ def render_settings():
|
|
|
5439
5537
|
key=f"settings_{key}",
|
|
5440
5538
|
)
|
|
5441
5539
|
|
|
5442
|
-
api_keys_tab, google_accounts_tab, material_sources_tab, ai_influencers_tab, voice_test_tab = render_localized_tabs(["API Keys", "Contas Google", "Fontes de Materiais", "AI Influencers", "Teste de Voz"])
|
|
5540
|
+
api_keys_tab, google_accounts_tab, tiktok_api_tab, material_sources_tab, ai_influencers_tab, voice_test_tab = render_localized_tabs(["API Keys", "Contas Google", "API Tiktok", "Fontes de Materiais", "AI Influencers", "Teste de Voz"])
|
|
5443
5541
|
|
|
5444
5542
|
with api_keys_tab:
|
|
5445
5543
|
with st.container(border=True):
|
|
@@ -5590,18 +5688,6 @@ def render_settings():
|
|
|
5590
5688
|
widget_key="api_test_voice_suno",
|
|
5591
5689
|
)
|
|
5592
5690
|
|
|
5593
|
-
with st.expander("TikTok for Developers — Client ID e Client Secret", expanded=False):
|
|
5594
|
-
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.")
|
|
5595
|
-
tiktok_client_key = text_setting("TikTok Client ID", "tiktok_client_key", secret=True)
|
|
5596
|
-
_render_credential_status(tiktok_client_key)
|
|
5597
|
-
tiktok_client_secret = text_setting("TikTok Client Secret", "tiktok_client_secret", secret=True)
|
|
5598
|
-
_render_api_test_control(
|
|
5599
|
-
settings,
|
|
5600
|
-
"tiktok",
|
|
5601
|
-
lambda: test_tiktok_credentials(tiktok_client_key, tiktok_client_secret, settings.get("tiktok_access_token", "")),
|
|
5602
|
-
widget_key="api_test_tiktok",
|
|
5603
|
-
)
|
|
5604
|
-
|
|
5605
5691
|
with st.expander("Publicação através do Upload-Post", expanded=False):
|
|
5606
5692
|
upload_post_enabled = st.checkbox("Activar Upload-Post", bool(settings.get("upload_post_enabled", False)))
|
|
5607
5693
|
upload_post_api_key = text_setting("Upload-Post API key", "upload_post_api_key", secret=True)
|
|
@@ -5650,7 +5736,6 @@ def render_settings():
|
|
|
5650
5736
|
"elevenlabs_api_key": elevenlabs_api_key, "elevenlabs_model_id": elevenlabs_model_id,
|
|
5651
5737
|
"chatterbox_base_url": chatterbox_base_url, "chatterbox_api_key": chatterbox_api_key, "chatterbox_model_id": chatterbox_model_id,
|
|
5652
5738
|
"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,
|
|
5653
|
-
"tiktok_client_key": tiktok_client_key, "tiktok_client_secret": tiktok_client_secret,
|
|
5654
5739
|
"upload_post_enabled": upload_post_enabled, "upload_post_api_key": upload_post_api_key,
|
|
5655
5740
|
"upload_post_username": upload_post_username, "upload_post_platforms": upload_post_platforms,
|
|
5656
5741
|
"upload_post_auto_upload": upload_post_auto_upload,
|
|
@@ -5670,6 +5755,9 @@ def render_settings():
|
|
|
5670
5755
|
with google_accounts_tab:
|
|
5671
5756
|
render_google_accounts()
|
|
5672
5757
|
|
|
5758
|
+
with tiktok_api_tab:
|
|
5759
|
+
render_tiktok_api_cards(settings)
|
|
5760
|
+
|
|
5673
5761
|
with material_sources_tab:
|
|
5674
5762
|
render_material_source_api_keys(settings)
|
|
5675
5763
|
|
|
@@ -124,7 +124,9 @@ def test_media_provider_card(card: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
124
124
|
return _result("missing", "Complete a Base URL antes de testar.")
|
|
125
125
|
if provider not in {"inferenceport", "ollama", "lmstudio"} and not api_key:
|
|
126
126
|
return _missing("Introduza a API key/token antes de testar este provider.")
|
|
127
|
-
if
|
|
127
|
+
if provider == "heygen":
|
|
128
|
+
return _get(f"{base_url}/v3/users/me", headers={"X-Api-Key": api_key})
|
|
129
|
+
if not model and provider not in {"inferenceport", "cloudflare_workers_ai", "heygen"}:
|
|
128
130
|
return _result("missing", "Complete o modelo antes de testar.")
|
|
129
131
|
headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
|
|
130
132
|
if provider == "cloudflare_workers_ai":
|
package/hermes_ui/languages.py
CHANGED
|
@@ -114,7 +114,7 @@ _CORE_UI_TEXT_KEYS = (
|
|
|
114
114
|
"Início", "Niche Finder", "Pipeline", "Pipeline TikTok", "Automação", "Edição", "AI Influencers", "Configurações",
|
|
115
115
|
"Criação de Vídeos", "Criação de Músicas", "Roteiros", "Upload", "Prompts Master", "Contas TikTok", "Automação Youtube",
|
|
116
116
|
"Niche Finder Kaggle", "Niche Finder Apify", "Limpador de Metadados", "Cortes", "Editor Python", "Download Mídia",
|
|
117
|
-
"Personagens", "Redes Sociais", "Tutorial Meta", "Tutorial Supabase", "Canais Youtube", "Blueprints Youtube", "MCP", "Contas Google",
|
|
117
|
+
"Personagens", "Redes Sociais", "Tutorial Meta", "Tutorial Supabase", "Canais Youtube", "Blueprints Youtube", "MCP", "Contas Google", "API Tiktok",
|
|
118
118
|
"Configuração API", "Notificações", "Interface local para operação e automação de conteúdo faceless", "Canais", "activos",
|
|
119
119
|
"Tarefas", "total registado", "A fazer", "na pipeline", "Em execução", "a decorrer", "Concluídos", "artefactos prontos",
|
|
120
120
|
"Falhas", "requerem atenção", "Filas locais e dependências da cascata", "Niche", "Blueprints", "Brand", "Script", "Title",
|
|
@@ -153,6 +153,9 @@ UI_TRANSLATIONS: dict[str, dict[str, str]] = {
|
|
|
153
153
|
},
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
for _translation in UI_TRANSLATIONS.values():
|
|
157
|
+
_translation.setdefault("API Tiktok", "API Tiktok")
|
|
158
|
+
|
|
156
159
|
# Labels for the independent LLM provider cards.
|
|
157
160
|
_LLM_TRANSLATIONS: dict[str, dict[str, str]] = {
|
|
158
161
|
"pt": {
|
|
@@ -53,9 +53,13 @@ def _base_url(card: Mapping[str, Any]) -> str:
|
|
|
53
53
|
|
|
54
54
|
def _headers(card: Mapping[str, Any], *, fal: bool = False) -> dict[str, str]:
|
|
55
55
|
key = _api_key(card)
|
|
56
|
+
provider = str(card.get("provider") or "").strip().lower()
|
|
56
57
|
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
57
58
|
if key:
|
|
58
|
-
|
|
59
|
+
if provider == "heygen":
|
|
60
|
+
headers["X-Api-Key"] = key
|
|
61
|
+
else:
|
|
62
|
+
headers["Authorization"] = f"Key {key}" if fal else f"Bearer {key}"
|
|
59
63
|
return headers
|
|
60
64
|
|
|
61
65
|
|
|
@@ -347,6 +351,8 @@ def _video_endpoint(card: Mapping[str, Any]) -> str:
|
|
|
347
351
|
definition = media_provider_definition(card.get("provider"))
|
|
348
352
|
style = str(card.get("api_style") or definition.api_style)
|
|
349
353
|
base = _base_url(card)
|
|
354
|
+
if style == "heygen":
|
|
355
|
+
return f"{base}/v3/videos"
|
|
350
356
|
if style == "fal_queue":
|
|
351
357
|
if not _model(card):
|
|
352
358
|
raise MediaGenerationError("FAL AI requer o identificador da rota/modelo para gerar vídeo.")
|
|
@@ -374,6 +380,21 @@ def _video_request(card: dict[str, Any], prompt: str, image_url: str = "") -> An
|
|
|
374
380
|
}
|
|
375
381
|
if image_url:
|
|
376
382
|
body["image_url"] = image_url
|
|
383
|
+
if style == "heygen":
|
|
384
|
+
avatar_id = str(card.get("avatar_id") or "").strip()
|
|
385
|
+
if not avatar_id:
|
|
386
|
+
raise MediaGenerationError("HeyGen requer Avatar ID no cartão de media para gerar vídeo.")
|
|
387
|
+
heygen_body: dict[str, Any] = {
|
|
388
|
+
"type": "avatar",
|
|
389
|
+
"avatar_id": avatar_id,
|
|
390
|
+
"script": body["prompt"],
|
|
391
|
+
"aspect_ratio": str(card.get("aspect_ratio") or INTERNAL_VIDEO_ASPECT_RATIO),
|
|
392
|
+
"output_format": "mp4",
|
|
393
|
+
}
|
|
394
|
+
voice_id = str(card.get("voice_id") or "").strip()
|
|
395
|
+
if voice_id:
|
|
396
|
+
heygen_body["voice_id"] = voice_id
|
|
397
|
+
return requests.post(endpoint, headers=_headers(card), json=heygen_body, timeout=180)
|
|
377
398
|
if style == "fal_queue":
|
|
378
399
|
body.pop("model", None)
|
|
379
400
|
return requests.post(endpoint, headers=_headers(card, fal=True), json=body, timeout=180)
|
|
@@ -403,6 +424,14 @@ def _video_result(payload: Mapping[str, Any]) -> tuple[str, str]:
|
|
|
403
424
|
if value:
|
|
404
425
|
return "", value
|
|
405
426
|
data = payload.get("data")
|
|
427
|
+
if isinstance(data, Mapping):
|
|
428
|
+
direct = str(data.get("video_url") or data.get("url") or "").strip()
|
|
429
|
+
if direct.startswith(("http://", "https://")):
|
|
430
|
+
return direct, ""
|
|
431
|
+
for key in ("video_id", "id", "request_id", "task_id", "job_id"):
|
|
432
|
+
value = str(data.get(key) or "").strip()
|
|
433
|
+
if value:
|
|
434
|
+
return "", value
|
|
406
435
|
if isinstance(data, list) and data and isinstance(data[0], Mapping):
|
|
407
436
|
first = data[0]
|
|
408
437
|
direct = str(first.get("url") or first.get("video_url") or "").strip()
|
|
@@ -422,6 +451,8 @@ def _poll_video(card: Mapping[str, Any], request_id: str, *, attempts: int = 24,
|
|
|
422
451
|
endpoint = f"{base}/requests/{request_id}/status"
|
|
423
452
|
elif style == "replicate":
|
|
424
453
|
endpoint = f"{base}/predictions/{request_id}"
|
|
454
|
+
elif style == "heygen":
|
|
455
|
+
endpoint = f"{base}/v3/videos/{request_id}"
|
|
425
456
|
else:
|
|
426
457
|
endpoint = f"{base}/videos/{request_id}"
|
|
427
458
|
headers = _headers(card, fal=style == "fal_queue")
|
|
@@ -437,8 +468,14 @@ def _poll_video(card: Mapping[str, Any], request_id: str, *, attempts: int = 24,
|
|
|
437
468
|
url, _ = _video_result(payload if isinstance(payload, Mapping) else {})
|
|
438
469
|
if url:
|
|
439
470
|
return url
|
|
440
|
-
|
|
471
|
+
status_payload = payload.get("data") if isinstance(payload, Mapping) and isinstance(payload.get("data"), Mapping) else payload
|
|
472
|
+
status = str((status_payload or {}).get("status") or "").lower() if isinstance(status_payload, Mapping) else ""
|
|
441
473
|
if status in {"failed", "error", "cancelled", "canceled"}:
|
|
474
|
+
if style == "heygen" and isinstance(status_payload, Mapping):
|
|
475
|
+
code = str(status_payload.get("failure_code") or "").strip()
|
|
476
|
+
detail = str(status_payload.get("failure_message") or "").strip()
|
|
477
|
+
suffix = f" ({code})" if code else ""
|
|
478
|
+
raise ProviderCallError(f"HeyGen marcou a tarefa como falhada{suffix}: {detail or 'sem detalhe'}", category="provider", retryable=False)
|
|
442
479
|
raise ProviderCallError("O provider marcou a tarefa de vídeo como falhada.", category="provider", retryable=False)
|
|
443
480
|
if index + 1 < attempts:
|
|
444
481
|
time.sleep(max(0.2, interval_seconds))
|
|
@@ -474,7 +511,7 @@ def generate_video_for_card(
|
|
|
474
511
|
raise MediaGenerationError(f"O provider {provider} não devolveu URL nem identificador de vídeo.")
|
|
475
512
|
destination = output_path or (STORAGE / "videos" / f"media-{provider}-{abs(hash((prompt, url))) & 0xffffffffffffffff:x}.mp4")
|
|
476
513
|
try:
|
|
477
|
-
response = requests.get(url, headers=
|
|
514
|
+
response = requests.get(url, headers=_headers(routed.card), timeout=300)
|
|
478
515
|
response.raise_for_status()
|
|
479
516
|
except requests.RequestException as exc:
|
|
480
517
|
raise MediaGenerationError(f"Não foi possível descarregar o vídeo gerado: {exc}") from exc
|
|
@@ -12,6 +12,18 @@ MEDIA_VIDEO_ACTIVE_CARD_KEY = "media_video_active_card_id"
|
|
|
12
12
|
MEDIA_IMAGE_ACTIVE_PROVIDER_KEY = "media_image_provider"
|
|
13
13
|
MEDIA_VIDEO_ACTIVE_PROVIDER_KEY = "media_video_provider"
|
|
14
14
|
|
|
15
|
+
FULL_IA_VIDEO_PROVIDER_CODES = (
|
|
16
|
+
"fal_ai",
|
|
17
|
+
"kie_ai",
|
|
18
|
+
"agnes",
|
|
19
|
+
"nano_banana",
|
|
20
|
+
"replicate",
|
|
21
|
+
"pollinations",
|
|
22
|
+
"huggingface",
|
|
23
|
+
"inferenceport",
|
|
24
|
+
"heygen",
|
|
25
|
+
)
|
|
26
|
+
|
|
15
27
|
# Mantidos internamente para preservar compatibilidade e controlar o prompt dos providers.
|
|
16
28
|
INTERNAL_IMAGE_ASPECT_RATIO = "16:9"
|
|
17
29
|
INTERNAL_IMAGE_SIZE = "1K"
|
|
@@ -55,7 +67,7 @@ MEDIA_PROVIDER_CATALOG: tuple[MediaProviderDefinition, ...] = (
|
|
|
55
67
|
),
|
|
56
68
|
MediaProviderDefinition(
|
|
57
69
|
"replicate",
|
|
58
|
-
"Replicate",
|
|
70
|
+
"Replicate AI",
|
|
59
71
|
default_base_url="https://api.replicate.com/v1",
|
|
60
72
|
supports_image=True,
|
|
61
73
|
supports_video=True,
|
|
@@ -133,6 +145,15 @@ MEDIA_PROVIDER_CATALOG: tuple[MediaProviderDefinition, ...] = (
|
|
|
133
145
|
api_style="fal_queue",
|
|
134
146
|
description="Model APIs da FAL com queue, status e resultado.",
|
|
135
147
|
),
|
|
148
|
+
MediaProviderDefinition(
|
|
149
|
+
"heygen",
|
|
150
|
+
"HeyGen",
|
|
151
|
+
default_base_url="https://api.heygen.com",
|
|
152
|
+
supports_video=True,
|
|
153
|
+
api_style="heygen",
|
|
154
|
+
extra_fields=("avatar_id", "voice_id"),
|
|
155
|
+
description="Vídeos com avatar, script ou áudio pela API V3 da HeyGen.",
|
|
156
|
+
),
|
|
136
157
|
)
|
|
137
158
|
|
|
138
159
|
_MEDIA_BY_CODE = {item.code: item for item in MEDIA_PROVIDER_CATALOG}
|
|
@@ -19,7 +19,7 @@ from hermes_ui.script_generation import generate_script_document
|
|
|
19
19
|
from hermes_ui.storage import STORAGE, ensure_storage, read_json, write_json
|
|
20
20
|
from hermes_ui.llm_providers import active_llm_card, provider_definition
|
|
21
21
|
from hermes_ui.media_generation import MediaGenerationError, _append_generation_constraints, generate_image_from_pool, generate_video_from_pool
|
|
22
|
-
from hermes_ui.media_providers import media_cards_for_pool
|
|
22
|
+
from hermes_ui.media_providers import FULL_IA_VIDEO_PROVIDER_CODES, media_cards_for_pool, media_provider_definition
|
|
23
23
|
from hermes_ui.material_sources import material_api_keys, selected_material_source
|
|
24
24
|
from hermes_ui.thumbnail_generation import ThumbnailGenerationError, generate_thumbnail_image
|
|
25
25
|
|
|
@@ -369,7 +369,10 @@ def _failure_attribution(
|
|
|
369
369
|
"failure_stage": stage,
|
|
370
370
|
}
|
|
371
371
|
if stage == "video" and route == "full_ia":
|
|
372
|
-
providers = [
|
|
372
|
+
providers = [
|
|
373
|
+
(code, media_provider_definition(code).label)
|
|
374
|
+
for code in FULL_IA_VIDEO_PROVIDER_CODES
|
|
375
|
+
]
|
|
373
376
|
found = [label for code, label in providers if code in combined or label.casefold() in combined]
|
|
374
377
|
labels = found or [label for _, label in providers]
|
|
375
378
|
codes = [code for code, label in providers if label in labels] or [code for code, _ in providers]
|
|
@@ -1060,7 +1063,7 @@ def _run_task(task: dict[str, Any]) -> dict[str, Any]:
|
|
|
1060
1063
|
video_path = generate_video_from_pool(
|
|
1061
1064
|
settings,
|
|
1062
1065
|
video_prompt,
|
|
1063
|
-
allowed_providers=
|
|
1066
|
+
allowed_providers=set(FULL_IA_VIDEO_PROVIDER_CODES),
|
|
1064
1067
|
)
|
|
1065
1068
|
else:
|
|
1066
1069
|
video_path = _run_video_helper({
|
|
@@ -1073,7 +1076,7 @@ def _run_task(task: dict[str, Any]) -> dict[str, Any]:
|
|
|
1073
1076
|
})
|
|
1074
1077
|
except MediaGenerationError as exc:
|
|
1075
1078
|
if route == "full_ia":
|
|
1076
|
-
message = f"Pool Full IA (FAL/KIE AI/Agnes AI): {exc}"
|
|
1079
|
+
message = f"Pool Full IA (FAL AI/KIE AI/Agnes AI/Nano Banana/Replicate AI/Pollinations.ai/Hugging Face Inference API/InferencePort Proxy/HeyGen): {exc}"
|
|
1077
1080
|
else:
|
|
1078
1081
|
message = f"Pipeline MoneyPrinterTurbo ({route}): {exc}"
|
|
1079
1082
|
metadata = _failure_attribution(task, settings, "video", error=str(exc))
|
package/hermes_ui/storage.py
CHANGED
|
@@ -287,6 +287,7 @@ DEFAULTS: dict[str, Any] = {
|
|
|
287
287
|
"postiz_auto_publish": False,
|
|
288
288
|
"tiktok_client_key": "",
|
|
289
289
|
"tiktok_client_secret": "",
|
|
290
|
+
"tiktok_api_cards": [],
|
|
290
291
|
"tiktok_accounts": [],
|
|
291
292
|
"tiktok_redirect_uri": "http://localhost:3030/oauth/tiktok/callback",
|
|
292
293
|
"tiktok_scopes": "user.info.basic,video.publish,video.upload",
|
|
@@ -528,8 +528,18 @@ class YouTubeAdapter:
|
|
|
528
528
|
|
|
529
529
|
class TikTokAdapter:
|
|
530
530
|
def __init__(self, settings: dict[str, Any]):
|
|
531
|
-
|
|
532
|
-
|
|
531
|
+
cards = settings.get("tiktok_api_cards") if isinstance(settings.get("tiktok_api_cards"), list) else []
|
|
532
|
+
selected = next(
|
|
533
|
+
(
|
|
534
|
+
card for card in cards
|
|
535
|
+
if isinstance(card, dict)
|
|
536
|
+
and str(card.get("client_id") or card.get("client_key") or "").strip()
|
|
537
|
+
and str(card.get("client_secret") or "").strip()
|
|
538
|
+
),
|
|
539
|
+
{},
|
|
540
|
+
)
|
|
541
|
+
self.client_key = str(selected.get("client_id") or selected.get("client_key") or settings.get("tiktok_client_key", "")).strip()
|
|
542
|
+
self.client_secret = str(selected.get("client_secret") or settings.get("tiktok_client_secret", "")).strip()
|
|
533
543
|
# Redirect URI, scopes, OAuth e access token são geridos no TikTok for Developers Playground.
|
|
534
544
|
# A UI guarda apenas as credenciais da aplicação.
|
|
535
545
|
self.access_token = os.getenv("TIKTOK_ACCESS_TOKEN", "")
|
package/package.json
CHANGED