@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.
- package/MANUAL-INSTALACAO.md +7 -1
- package/README.md +6 -0
- package/app/main.py +37 -3
- package/hermes_ui/pipeline_worker.py +16 -8
- package/package.json +1 -1
- package/scripts/cli.mjs +11 -2
- package/seed/skills/mpt_agent.py +29 -15
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.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
|
-
|
|
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(
|
|
5393
|
-
|
|
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
|
|
321
|
-
|
|
322
|
-
lines = [line for line in
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
|
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] = {
|
|
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
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", () =>
|
|
209
|
+
upstreamSocket.on("error", () => {
|
|
210
|
+
if (!clientSocket.destroyed) clientSocket.destroy();
|
|
211
|
+
});
|
|
205
212
|
});
|
|
206
213
|
|
|
207
|
-
|
|
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"], {
|
package/seed/skills/mpt_agent.py
CHANGED
|
@@ -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
|
|
628
|
+
"""Run dependency sync with a bounded wait and safe failure output."""
|
|
629
629
|
log("installing or verifying project dependencies with uv")
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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 =
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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(
|