@danhachuel/thunderbolt 0.3.64 → 0.3.66

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.
@@ -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.64
5
+ > **Versão deste manual:** 0.3.66
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.64 install
131
+ npx.cmd --yes --prefer-online @danhachuel/thunderbolt@0.3.66 install
132
132
  ```
133
133
 
134
134
  Linux/macOS:
135
135
 
136
136
  ```bash
137
- npx --yes --prefer-online @danhachuel/thunderbolt@0.3.64 install
137
+ npx --yes --prefer-online @danhachuel/thunderbolt@0.3.66 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.64 install --purge-data
145
+ npx.cmd --yes --prefer-online @danhachuel/thunderbolt@0.3.66 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,8 +395,12 @@ 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
+
398
400
  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
401
 
402
+ 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.
403
+
400
404
  As notificações novas são verificadas automaticamente na sessão activa e aparecem como pop-ups no canto inferior direito, mesmo quando está aberta outra página. O pop-up não marca o registo como lido: a aba **Notificações** continua a ser o centro persistente para consultar o histórico, gerir preferências e marcar eventos como lidos. Cada ID é apresentado no máximo uma vez por sessão do navegador.
401
405
 
402
406
  | Provider | Configuração mínima | Pool suportado |
@@ -524,7 +528,9 @@ Após iniciar a aplicação, valide o seguinte percurso:
524
528
 
525
529
  ## Configuração API — fontes de materiais
526
530
 
527
- 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.
531
+ 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.
532
+
533
+ 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.
528
534
 
529
535
  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.
530
536
 
@@ -556,7 +562,7 @@ Ao abrir a página, o Thunderbolt não prepara dados públicos, não descarrega
556
562
 
557
563
  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.
558
564
 
559
- As dependências adicionais — `scikit-learn`, `mlxtend`, `plotly`, `seaborn`, `matplotlib` e `kagglehub` — são instaladas pelo procedimento normal de `npx`. Em instalações existentes, execute novamente `npx.cmd --yes --prefer-online @danhachuel/thunderbolt@0.3.64 install`; o instalador detecta e reutiliza o que já estiver válido.
565
+ 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.66 install`; o instalador detecta e reutiliza o que já estiver válido.
560
566
 
561
567
  ### Niche Finder Apify
562
568
 
package/README.md CHANGED
@@ -42,9 +42,11 @@ 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 exclusivamente os cartões activos FAL AI, KIE AI e Agnes AI do pool de vídeo. **Apenas Música** não chama a pipeline de vídeo nem tenta gerar thumbnail: reutiliza o áudio local/Suno já descarregado e deixa-o pronto para a integração de upload musical.
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
- 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. 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.
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
+
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.
48
50
 
49
51
  ## Upload-Post — publicação para múltiplas plataformas
50
52
 
@@ -87,7 +89,7 @@ No topo da área principal da aplicação existe o menu nativo de idioma no padr
87
89
 
88
90
  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
91
 
90
- ## Navegação da UI 0.3.64
92
+ ## Navegação da UI 0.3.66
91
93
 
92
94
  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
95
  **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
@@ -164,7 +164,7 @@ VIDEO_CONCATENATION_OPTIONS = ["Random Concatenation (Recommended)", "Sequential
164
164
  VIDEO_TRANSITION_OPTIONS = ["None", "Fade", "Dissolve"]
165
165
  VIDEO_ENCODER_OPTIONS = ["Default (Recommended)", "H.264", "H.265"]
166
166
  VOICEOVER_MODE_OPTIONS = ["Auto", "Upload", "None"]
167
- VOICEOVER_SERVICE_OPTIONS = ["Azure TTS V1"]
167
+ VOICEOVER_SERVICE_OPTIONS = ["Azure Speech SDK V2", "Azure TTS V1"]
168
168
  VOICEOVER_VOLUME_OPTIONS = ["20%", "40%", "60%", "80%", "100%"]
169
169
  VOICEOVER_SPEED_OPTIONS = ["0.5x", "0.75x", "1.0x", "1.25x", "1.5x", "2.0x"]
170
170
  BACKGROUND_MUSIC_SOURCE_OPTIONS = ["Ficheiro existente", "Carregar ficheiro", "Criar via Suno API", "Random Background Music", "Sem música"]
@@ -1046,7 +1046,20 @@ def render_video_generation_settings(
1046
1046
  if stored_voiceover_value and Path(stored_voiceover_value).is_file():
1047
1047
  st.audio(stored_voiceover_value)
1048
1048
  settings["voiceover_file"] = stored_voiceover_value
1049
- settings["voiceover_service"] = st.selectbox("Voiceover Service", VOICEOVER_SERVICE_OPTIONS, key=f"{prefix}_voiceover_service")
1049
+ configured_voice_settings = read_json("settings.json", {})
1050
+ configured_azure_voice = bool(
1051
+ str(configured_voice_settings.get("azure_speech_key") or "").strip()
1052
+ and str(configured_voice_settings.get("azure_speech_region") or "").strip()
1053
+ )
1054
+ voice_service_default = "Azure Speech SDK V2" if configured_azure_voice else "Azure TTS V1"
1055
+ voice_service_index = VOICEOVER_SERVICE_OPTIONS.index(voice_service_default)
1056
+ settings["voiceover_service"] = st.selectbox(
1057
+ "Voiceover Service",
1058
+ VOICEOVER_SERVICE_OPTIONS,
1059
+ index=voice_service_index,
1060
+ key=f"{prefix}_voiceover_service",
1061
+ help="Azure Speech SDK V2 usa a key/região configuradas e não depende do stream edge_tts. Azure TTS V1 usa edge_tts e pode funcionar sem key Azure.",
1062
+ )
1050
1063
  if channel is not None:
1051
1064
  channel_id = str(channel.get("id") or channel.get("name") or "")
1052
1065
  channel_voice = str(channel.get("default_voice") or channel.get("voice") or "").strip()
@@ -5381,7 +5394,8 @@ def render_media_provider_cards(settings: dict[str, Any], *, embedded: bool = Fa
5381
5394
  settings.update(migrated)
5382
5395
  write_json("settings.json", settings)
5383
5396
  with st.expander("Imagem e Video", expanded=False):
5384
- st.caption("Configure providers de imagem e vídeo em cartões independentes. O router usa apenas o pool correspondente e faz failover entre providers activos.")
5397
+ full_ia_labels = ", ".join(media_provider_definition(code).label for code in FULL_IA_VIDEO_PROVIDER_CODES)
5398
+ 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}.")
5385
5399
  image_cards = [card for card in cards if card.get("supports_image")]
5386
5400
  video_cards = [card for card in cards if card.get("supports_video")]
5387
5401
  selector_cols = st.columns(3)
@@ -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 not model and provider not in {"inferenceport", "cloudflare_workers_ai"}:
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":
@@ -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
- headers["Authorization"] = f"Key {key}" if fal else f"Bearer {key}"
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
- status = str((payload or {}).get("status") or "").lower() if isinstance(payload, Mapping) else ""
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={"Authorization": f"Bearer {_api_key(routed.card)}"} if _api_key(routed.card) else {}, timeout=300)
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
 
@@ -324,6 +324,16 @@ def _failure_attribution(
324
324
  provider_code = str(markers["helper_provider"] or "").strip().casefold()
325
325
  combined = f"{output} {error}".casefold()
326
326
 
327
+ if stage == "video" and any(marker in combined for marker in ("edge_tts", "edge tts", "azure_tts_v1", "azure speech")):
328
+ return {
329
+ "failure_api": "Azure Speech / edge_tts API",
330
+ "failure_provider": "azure_speech, edge_tts",
331
+ "failure_service": "Narração TTS",
332
+ "failure_route": route,
333
+ "failure_config_fields": "",
334
+ "failure_stage": stage,
335
+ }
336
+
327
337
  if stage == "video" and missing:
328
338
  api_labels: list[str] = []
329
339
  provider_values: list[str] = []
@@ -359,7 +369,10 @@ def _failure_attribution(
359
369
  "failure_stage": stage,
360
370
  }
361
371
  if stage == "video" and route == "full_ia":
362
- providers = [("fal_ai", "FAL AI"), ("kie_ai", "KIE AI"), ("agnes", "Agnes AI")]
372
+ providers = [
373
+ (code, media_provider_definition(code).label)
374
+ for code in FULL_IA_VIDEO_PROVIDER_CODES
375
+ ]
363
376
  found = [label for code, label in providers if code in combined or label.casefold() in combined]
364
377
  labels = found or [label for _, label in providers]
365
378
  codes = [code for code, label in providers if label in labels] or [code for code, _ in providers]
@@ -567,7 +580,45 @@ def _mpt_video_language(value: Any) -> str:
567
580
  return raw if re.fullmatch(r"[A-Za-z]{2,3}(?:-[A-Za-z]{2,4})?", raw) else ""
568
581
 
569
582
 
570
- def _moneyprinter_cli_args(task: dict[str, Any], route: str) -> list[str]:
583
+ def _uses_azure_speech_sdk_v2(
584
+ generation_settings: dict[str, Any],
585
+ settings: dict[str, Any] | None = None,
586
+ ) -> bool:
587
+ service = str(generation_settings.get("voiceover_service") or "").strip().casefold()
588
+ if service in {
589
+ "azure speech sdk v2",
590
+ "azure speech",
591
+ "azure tts v2",
592
+ "azure speech sdk",
593
+ }:
594
+ return True
595
+ if service in {"azure tts v1", "edge tts", "edge_tts"}:
596
+ configured = settings or {}
597
+ return bool(
598
+ str(configured.get("azure_speech_key") or "").strip()
599
+ and str(configured.get("azure_speech_region") or "").strip()
600
+ )
601
+ configured = settings or {}
602
+ return bool(
603
+ str(configured.get("azure_speech_key") or "").strip()
604
+ and str(configured.get("azure_speech_region") or "").strip()
605
+ )
606
+
607
+
608
+ def _azure_speech_v2_voice_name(value: str) -> str:
609
+ """Mark a normal Azure voice for MPT's Azure Speech SDK V2 branch."""
610
+ voice = str(value or "").strip()
611
+ if not voice or ":" in voice or voice.casefold() == "no-voice":
612
+ return voice
613
+ if re.search(r"-v2(?:-|$)", voice, flags=re.IGNORECASE):
614
+ return voice
615
+ match = re.search(r"-(Female|Male)$", voice, flags=re.IGNORECASE)
616
+ if match:
617
+ return f"{voice[:match.start()]}-V2{voice[match.start():]}"
618
+ return f"{voice}-V2"
619
+
620
+
621
+ def _moneyprinter_cli_args(task: dict[str, Any], route: str, settings: dict[str, Any] | None = None) -> list[str]:
571
622
  """Build the explicit MPT CLI contract for the stock Pexels/Pixabay route."""
572
623
  generation_settings = task.get("generation_settings") if isinstance(task.get("generation_settings"), dict) else {}
573
624
  args: list[str] = ["--video-source", route]
@@ -606,6 +657,8 @@ def _moneyprinter_cli_args(task: dict[str, Any], route: str) -> list[str]:
606
657
  if voice_mode == "none" or voice_mode == "upload":
607
658
  args.extend(["--voice-name", "no-voice"])
608
659
  elif voice:
660
+ if _uses_azure_speech_sdk_v2(generation_settings, settings):
661
+ voice = _azure_speech_v2_voice_name(voice)
609
662
  args.extend(["--voice-name", voice])
610
663
  volume = generation_settings.get("voiceover_volume")
611
664
  speed = generation_settings.get("voiceover_speed")
@@ -680,7 +733,24 @@ def _run_video_helper(task: dict[str, Any]) -> Path:
680
733
  command.extend(["--root", str(configured_root)])
681
734
  command.extend(["--subject", subject, "--"])
682
735
  generation_settings = task.get("generation_settings") if isinstance(task.get("generation_settings"), dict) else {}
683
- command.extend(_moneyprinter_cli_args(task, route))
736
+ if _uses_azure_speech_sdk_v2(generation_settings, settings) and str(generation_settings.get("voiceover_mode") or "").strip().casefold() not in {"none", "upload"}:
737
+ missing_voice_config = [
738
+ field for field, value in (
739
+ ("azure_speech_key", settings.get("azure_speech_key")),
740
+ ("azure_speech_region", settings.get("azure_speech_region")),
741
+ ) if not str(value or "").strip()
742
+ ]
743
+ if missing_voice_config:
744
+ message = "Azure Speech SDK V2 foi seleccionado, mas faltam credenciais de voz."
745
+ metadata = _failure_attribution(task, settings, "video", error=message)
746
+ metadata.update({
747
+ "failure_api": "Azure Speech API",
748
+ "failure_provider": "azure_speech",
749
+ "failure_service": "Azure Speech SDK V2",
750
+ "failure_config_fields": ", ".join(missing_voice_config),
751
+ })
752
+ raise PipelineError(_failure_message(message, metadata), failure_metadata=metadata)
753
+ command.extend(_moneyprinter_cli_args(task, route, settings=settings))
684
754
  if str(generation_settings.get("voiceover_mode") or "").strip().casefold() == "upload":
685
755
  voiceover_file = Path(str(generation_settings.get("voiceover_file") or "").strip()).expanduser()
686
756
  if not str(voiceover_file) or not voiceover_file.is_file() or voiceover_file.stat().st_size <= 0:
@@ -993,7 +1063,7 @@ def _run_task(task: dict[str, Any]) -> dict[str, Any]:
993
1063
  video_path = generate_video_from_pool(
994
1064
  settings,
995
1065
  video_prompt,
996
- allowed_providers={"fal_ai", "kie_ai", "agnes"},
1066
+ allowed_providers=set(FULL_IA_VIDEO_PROVIDER_CODES),
997
1067
  )
998
1068
  else:
999
1069
  video_path = _run_video_helper({
@@ -1006,7 +1076,7 @@ def _run_task(task: dict[str, Any]) -> dict[str, Any]:
1006
1076
  })
1007
1077
  except MediaGenerationError as exc:
1008
1078
  if route == "full_ia":
1009
- 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}"
1010
1080
  else:
1011
1081
  message = f"Pipeline MoneyPrinterTurbo ({route}): {exc}"
1012
1082
  metadata = _failure_attribution(task, settings, "video", error=str(exc))
@@ -116,6 +116,13 @@ def build_moneyprinter_config(settings: dict[str, Any], existing: dict[str, Any]
116
116
  for settings_key, config_key in app_map.items():
117
117
  if settings_key in settings:
118
118
  app[config_key] = settings[settings_key]
119
+ # Edge TTS V1 is retained as a keyless fallback, but its default 30-second
120
+ # stream deadline is too short for longer scripts or slow networks.
121
+ try:
122
+ edge_tts_timeout = float(settings.get("edge_tts_timeout", 90))
123
+ except (TypeError, ValueError):
124
+ edge_tts_timeout = 90.0
125
+ app["edge_tts_timeout"] = max(30, int(edge_tts_timeout))
119
126
  # A UI can store the canonical mapping while older installations still
120
127
  # expose one legacy field per source. Export every supported source as a
121
128
  # TOML array so MoneyPrinterTurbo can rotate keys without CSV parsing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.3.64",
3
+ "version": "0.3.66",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",