@danhachuel/thunderbolt 0.3.77 → 0.3.79

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.77
5
+ > **Versão deste manual:** 0.3.78
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)
@@ -817,3 +817,9 @@ No card **OpenAI / NVIDIA NIM**, o campo **Modelo** é uma lista suspensa. Use *
817
817
  Quando Azure Speech SDK V2 estiver seleccionado, o Thunderbolt prepara a narração em segmentos antes de iniciar o MoneyPrinterTurbo. O tamanho interno dos segmentos é reduzido automaticamente para velocidades de fala lentas, o áudio final é validado e a CLI recebe `--custom-audio-file`. A geração é bloqueada se não houver confirmação dos segmentos; isto evita regressar à chamada monolítica do Azure que pode falhar com `maximum media duration of 600000ms`.
818
818
 
819
819
  Na interface, uma tarefa em execução mostra a última actividade do helper e o tempo decorrido da etapa Vídeo. Se ocorrer uma falha, consulte o caminho `LOG_FILE` ou `RESULT_FILE` apresentado no erro. A linha `updated configuration fields` é apenas sincronização inicial de configuração e não é a causa terminal. Nunca copie chaves, cookies, tokens ou o conteúdo integral do roteiro para pedidos de suporte.
820
+
821
+ ## 17. Quando o card mostra apenas o início da falha de vídeo
822
+
823
+ A partir da versão 0.3.78, o card **Backlog Vídeos** mostra a causa terminal e disponibiliza **Ver diagnóstico completo**. As mensagens `Pexels key validation completed`, `installing or verifying project dependencies with uv` e `TASK_DIR` são apenas progresso ou metadados; não são, por si só, uma falha.
824
+
825
+ Se o `uv` falhar, o erro inclui o detalhe seguro do resolver. Se a CLI do MoneyPrinterTurbo falhar, o resumo inclui `MPT_ERROR` e mantém `Log completo` e `Manifesto`. A interface não exibe chaves, cookies ou tokens. O tempo limite da verificação de dependências é de 15 minutos; a etapa é encerrada em vez de ficar indefinidamente sem resposta.
package/README.md CHANGED
@@ -432,3 +432,9 @@ O campo **Modelo** em **Configuração API > API Keys > LLM — providers e mode
432
432
  No fluxo MoneyPrinterTurbo, a voz Azure Speech SDK V2 é preparada antes da CLI upstream. O Thunderbolt divide o roteiro em segmentos conservadores ajustados à velocidade, concatena o áudio localmente e só inicia o motor depois de confirmar o ficheiro e a contagem de segmentos. Se essa preparação falhar, a tarefa é interrompida e não existe fallback silencioso para uma chamada monolítica sujeita ao limite de 600000 ms.
433
433
 
434
434
  Durante a execução, o backlog mostra a actividade recente do helper e o tempo da etapa Vídeo. Em caso de falha, a mensagem terminal é extraída do output accionável, enquanto as linhas `using existing project` e `updated configuration fields` deixam de ser tratadas como causa. Os caminhos do log e do manifesto são preservados mesmo quando a falha ocorre antes da criação do MP4.
435
+
436
+ ## Correcção de diagnóstico da etapa Vídeo — 0.3.78
437
+
438
+ A mensagem do card de vídeo deixou de mostrar apenas os primeiros 240 caracteres. A UI apresenta um resumo de erro com até 700 caracteres e disponibiliza um expander não editável com o diagnóstico completo, incluindo os caminhos do log e do manifesto quando existem. Linhas de sucesso de Pexels, sincronização do projecto, verificação do `uv` e marcadores de caminho são excluídas do resumo terminal.
439
+
440
+ A verificação `uv sync --frozen` tem agora timeout controlado, detalhe seguro do resolver em caso de falha e confirmação explícita quando termina. O estado do manifesto é marcado como `failed` mesmo quando a falha acontece antes de a CLI do MoneyPrinterTurbo iniciar.
package/app/main.py CHANGED
@@ -3592,7 +3592,15 @@ def _render_video_task_state(task: dict[str, Any]) -> None:
3592
3592
  except (TypeError, ValueError):
3593
3593
  pass
3594
3594
  if task.get("error"):
3595
- st.caption(str(task.get("error"))[:240])
3595
+ error_text = str(task.get("error") or "").strip()
3596
+ st.error(error_text[:700])
3597
+ if len(error_text) > 700 or task.get("video_log") or task.get("video_result"):
3598
+ with st.expander("Ver diagnóstico completo", expanded=False):
3599
+ st.code(error_text[:6000])
3600
+ if task.get("video_log"):
3601
+ st.caption(f"Log completo: {task.get('video_log')}")
3602
+ if task.get("video_result"):
3603
+ st.caption(f"Manifesto: {task.get('video_result')}")
3596
3604
 
3597
3605
 
3598
3606
  def render_videos():
@@ -5389,11 +5397,25 @@ def _render_api_test_feedback(settings: dict[str, Any], test_key: str, result: d
5389
5397
  st.error(ui_text("Último teste: chamada falhou", language) + suffix)
5390
5398
 
5391
5399
 
5392
- def _render_api_test_control(settings: dict[str, Any], test_key: str, callback: Any, *, widget_key: str) -> None:
5393
- """Render a form-safe diagnostic button and persist its redacted result."""
5400
+ def _render_api_test_control(
5401
+ settings: dict[str, Any],
5402
+ test_key: str,
5403
+ callback: Any,
5404
+ *,
5405
+ widget_key: str,
5406
+ persist_callback: Any = None,
5407
+ ) -> None:
5408
+ """Render a form-safe diagnostic button and persist its redacted result.
5409
+
5410
+ ``persist_callback`` keeps a card whose fields live in the global form
5411
+ consistent: testing it must not appear successful while its values remain
5412
+ only in the browser session.
5413
+ """
5394
5414
  if st.form_submit_button("Testar chamada API", use_container_width=True, key=widget_key):
5395
5415
  with st.spinner(ui_text("A testar chamada API…", current_ui_language())):
5396
5416
  try:
5417
+ if persist_callback is not None:
5418
+ persist_callback()
5397
5419
  result = callback()
5398
5420
  except Exception:
5399
5421
  result = {"status": "error", "message": "A chamada de diagnóstico falhou."}
@@ -5824,11 +5846,23 @@ def render_settings():
5824
5846
  azure_speech_key = text_setting("Azure Speech key", "azure_speech_key", secret=True)
5825
5847
  _render_credential_status(azure_speech_key)
5826
5848
  azure_speech_region = text_setting("Azure Speech region", "azure_speech_region")
5849
+
5850
+ def save_azure_speech() -> None:
5851
+ settings.update({
5852
+ "azure_speech_key": azure_speech_key.strip(),
5853
+ "azure_speech_region": azure_speech_region.strip(),
5854
+ })
5855
+ write_json("settings.json", settings)
5856
+
5857
+ if st.form_submit_button("Guardar Azure Speech", type="primary", use_container_width=True, key="save_azure_speech"):
5858
+ save_azure_speech()
5859
+ st.success("Azure Speech guardado.")
5827
5860
  _render_api_test_control(
5828
5861
  settings,
5829
5862
  "voice:azure_speech",
5830
5863
  lambda: test_voice_provider("azure_speech", {"azure_speech_key": azure_speech_key, "azure_speech_region": azure_speech_region}),
5831
5864
  widget_key="api_test_voice_azure",
5865
+ persist_callback=save_azure_speech,
5832
5866
  )
5833
5867
 
5834
5868
  with st.container(border=True):
@@ -317,12 +317,15 @@ def _redact_helper_output(text: str) -> str:
317
317
  return text
318
318
 
319
319
  def _terminal_helper_detail(output: str) -> str:
320
- """Return the last actionable helper lines, not startup/configuration noise."""
321
- lines = [_redact_helper_output(line).strip() for line in str(output or "").splitlines()]
322
- lines = [line for line in lines if line]
323
- noise_markers = ("using existing project:", "updated configuration fields:", "starting video generation, task id:")
324
- actionable = [line for line in lines if not any(marker in line.casefold() for marker in noise_markers)]
325
- return "\n".join((actionable or lines)[-12:]).strip()
320
+ """Return concise actionable failure lines, never startup or path markers."""
321
+ raw_lines = [_redact_helper_output(line).strip() for line in str(output or "").splitlines()]
322
+ lines = [line for line in raw_lines if line]
323
+ metadata_prefixes = ("TASK_DIR=", "LOG_FILE=", "RESULT_FILE=", "VIDEO_FILE=")
324
+ noise_markers = ("using existing project:", "updated configuration fields:", "starting video generation, task id:", "installing or verifying project dependencies with uv", "pexels key validation completed:")
325
+ useful = [line for line in lines if not line.startswith(metadata_prefixes) and not any(marker in line.casefold() for marker in noise_markers)]
326
+ failure_markers = ("mpt_error=", "traceback", "exception", "error", "failed", "falhou", "failure", "missing", "invalid", "timeout", "timed out", "not found", "exceeded", "excedeu")
327
+ failures = [line for line in useful if any(marker in line.casefold() for marker in failure_markers)]
328
+ return "\n".join((failures or useful)[-8:]).strip()
326
329
 
327
330
 
328
331
  def _helper_failure_markers(output: str) -> dict[str, Any]:
@@ -522,12 +525,13 @@ def _failure_message(message: str, metadata: dict[str, Any]) -> str:
522
525
 
523
526
 
524
527
  def _persist_video_diagnostics(task: dict[str, Any], output: str) -> dict[str, str]:
525
- """Persist only bounded helper diagnostics and return its declared file paths."""
528
+ """Persist bounded helper diagnostics and a safe terminal summary."""
526
529
  task_id = str(task.get("id") or "").strip()
527
530
  if not task_id:
528
531
  return {}
529
532
  log_file = _helper_output_value(output, "LOG_FILE")
530
533
  result_file = _helper_output_value(output, "RESULT_FILE")
534
+ terminal_detail = _terminal_helper_detail(output)
531
535
  try:
532
536
  markers = _helper_failure_markers(output)
533
537
  payload: dict[str, Any] = {
@@ -535,6 +539,7 @@ def _persist_video_diagnostics(task: dict[str, Any], output: str) -> dict[str, s
535
539
  "log_file": log_file,
536
540
  "result_file": result_file,
537
541
  "output_tail": _redact_helper_output(output[-6000:]),
542
+ "terminal_detail": terminal_detail,
538
543
  "helper_provider": markers["helper_provider"],
539
544
  "missing_fields": markers["missing_fields"],
540
545
  "invalid_fields": markers["invalid_fields"],
@@ -543,7 +548,10 @@ def _persist_video_diagnostics(task: dict[str, Any], output: str) -> dict[str, s
543
548
  current = _task_by_id(task_id) or task
544
549
  artifacts = dict(current.get("artifacts") or {})
545
550
  artifacts["video_diagnostics"] = artifact_path
546
- updates: dict[str, Any] = {"artifacts": artifacts}
551
+ updates: dict[str, Any] = {
552
+ "artifacts": artifacts,
553
+ "video_diagnostic_summary": terminal_detail,
554
+ }
547
555
  if log_file:
548
556
  updates["video_log"] = log_file
549
557
  artifacts["video_log"] = log_file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.3.77",
3
+ "version": "0.3.79",
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",
package/scripts/cli.mjs CHANGED
@@ -193,6 +193,11 @@ const proxy = http.createServer((request, response) => {
193
193
  });
194
194
 
195
195
  proxy.on("upgrade", (request, clientSocket, head) => {
196
+ // Clientes remotos podem encerrar o WebSocket antes de o Streamlit concluir
197
+ // a resposta. Tratar ECONNRESET impede que o Node termine o launcher.
198
+ clientSocket.on("error", () => {
199
+ if (!clientSocket.destroyed) clientSocket.destroy();
200
+ });
196
201
  const upstreamSocket = net.connect(backendPort, "127.0.0.1", () => {
197
202
  const headers = Object.entries(request.headers)
198
203
  .map(([name, value]) => `${name}: ${Array.isArray(value) ? value.join(", ") : value}`)
@@ -201,10 +206,14 @@ proxy.on("upgrade", (request, clientSocket, head) => {
201
206
  if (head.length) upstreamSocket.write(head);
202
207
  clientSocket.pipe(upstreamSocket).pipe(clientSocket);
203
208
  });
204
- upstreamSocket.on("error", () => clientSocket.destroy());
209
+ upstreamSocket.on("error", () => {
210
+ if (!clientSocket.destroyed) clientSocket.destroy();
211
+ });
205
212
  });
206
213
 
207
- proxy.listen(publicPort, "127.0.0.1", () => {
214
+ // O backend Streamlit fica em 127.0.0.1; apenas o proxy escuta em todas as
215
+ // interfaces para que o encaminhamento seguro do ambiente consiga alcançá-lo.
216
+ proxy.listen(publicPort, () => {
208
217
  console.log(`Thunderbolt: interface disponível em http://localhost:${publicPort}/`);
209
218
  });
210
219
  const worker = spawn(python, ["-m", "hermes_ui.automation_worker"], {
@@ -25,6 +25,7 @@ PROJECT_ARCHIVE_URL = (
25
25
  )
26
26
  DEFAULT_ROOT = Path.home() / "MoneyPrinterTurbo"
27
27
  CHUNKED_SYNTHESIS_TIMEOUT_SECONDS = 15 * 60
28
+ DEPENDENCY_SYNC_TIMEOUT_SECONDS = 15 * 60
28
29
  DEFAULT_VOICE_NAME = "zh-CN-XiaoxiaoNeural-Female"
29
30
  NEEDS_INPUT_EXIT_CODE = 10
30
31
  SUPPORTED_SOURCES = {"pexels", "pixabay", "coverr", "local"}
@@ -623,24 +624,37 @@ def write_result_manifest(root: Path, payload: dict[str, object]) -> Path:
623
624
  temp_path.replace(result_path)
624
625
  return result_path.resolve()
625
626
 
626
-
627
627
  def run_checked(command: list[str], *, cwd: Path) -> None:
628
- """Run dependency sync quietly and show only the last 30 lines on failure."""
628
+ """Run dependency sync with a bounded wait and safe failure output."""
629
629
  log("installing or verifying project dependencies with uv")
630
- result = subprocess.run(
631
- command,
632
- cwd=cwd,
633
- stdout=subprocess.PIPE,
634
- stderr=subprocess.STDOUT,
635
- text=True,
636
- errors="replace",
637
- check=False,
638
- )
630
+ try:
631
+ result = subprocess.run(
632
+ command,
633
+ cwd=cwd,
634
+ stdout=subprocess.PIPE,
635
+ stderr=subprocess.STDOUT,
636
+ text=True,
637
+ errors="replace",
638
+ check=False,
639
+ timeout=DEPENDENCY_SYNC_TIMEOUT_SECONDS,
640
+ )
641
+ except subprocess.TimeoutExpired as exc:
642
+ raise SkillError(
643
+ "A verificação das dependências com uv excedeu 15 minutos; "
644
+ "a execução foi interrompida."
645
+ ) from exc
646
+ output = result.stdout or ""
647
+ for key in ("MPT_LLM_API_KEY", "MPT_PEXELS_API_KEY", "MPT_PIXABAY_API_KEY", "AZURE_SPEECH_KEY", "AZURE_SPEECH_REGION"):
648
+ secret = os.environ.get(key, "").strip()
649
+ if secret:
650
+ output = output.replace(secret, "[redacted]")
639
651
  if result.returncode != 0:
640
- output_tail = (result.stdout or "").splitlines()[-30:]
641
- if output_tail:
642
- print("\n".join(output_tail), file=sys.stderr)
643
- raise SkillError(f"dependency installation failed with exit code {result.returncode}")
652
+ output_tail = "\n".join(output.splitlines()[-30:]).strip()
653
+ raise SkillError(
654
+ f"dependency installation failed with exit code {result.returncode}. "
655
+ + (f"Detalhe do uv: {output_tail}" if output_tail else "O uv não devolveu detalhes.")
656
+ )
657
+ log("project dependencies verified with uv")
644
658
 
645
659
 
646
660
  def generate_video(