@danhachuel/thunderbolt 0.3.83 → 0.3.85

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/app/main.py CHANGED
@@ -3505,7 +3505,7 @@ def _render_pipeline_worker_banner(worker_status: dict[str, Any], active_count:
3505
3505
  st.warning("Worker de vídeo sem heartbeat recente. O launcher deve estar aberto para processar as tarefas.")
3506
3506
  heartbeat = worker_status.get("last_heartbeat_at") or worker_status.get("updated_at")
3507
3507
  if heartbeat:
3508
- st.caption(f"{_pipeline_time_age(heartbeat)} · timeout de execução: {STALE_TASK_SECONDS // 60} minutos")
3508
+ st.caption(f"{_pipeline_time_age(heartbeat)} · vídeo: até 90 min com watchdog de inactividade de 10 min")
3509
3509
  if worker_status.get("last_error"):
3510
3510
  st.error(f"Último erro do worker: {worker_status['last_error']}")
3511
3511
 
@@ -28,7 +28,8 @@ from hermes_ui.thumbnail_generation import ThumbnailGenerationError, generate_th
28
28
  PIPELINE_LOCK_FILENAME = "pipeline_worker.lock"
29
29
  PIPELINE_LOG_FILENAME = "pipeline_worker.json"
30
30
  VIDEO_TIMEOUT_SECONDS = 20 * 60
31
- LONG_STOCK_VIDEO_TIMEOUT_SECONDS = 45 * 60
31
+ LONG_STOCK_VIDEO_TIMEOUT_SECONDS = 90 * 60
32
+ VIDEO_IDLE_TIMEOUT_SECONDS = 10 * 60
32
33
  STALE_TASK_SECONDS = VIDEO_TIMEOUT_SECONDS + 5 * 60
33
34
  WORKER_HEARTBEAT_TIMEOUT_SECONDS = 15
34
35
  CASCADE_STAGE_ORDER = ("topic", "script", "title", "keywords", "video", "thumbnail_prompt", "thumbnail", "upload")
@@ -636,6 +637,17 @@ def _task_stale_timeout_seconds(task: dict[str, Any]) -> int:
636
637
  return STALE_TASK_SECONDS
637
638
 
638
639
 
640
+ def _latest_helper_log_activity(log_path: Path | None, previous_mtime: float) -> tuple[float, bool]:
641
+ """Return whether the helper's file log advanced without reading its content."""
642
+ if log_path is None:
643
+ return previous_mtime, False
644
+ try:
645
+ current_mtime = log_path.stat().st_mtime
646
+ except OSError:
647
+ return previous_mtime, False
648
+ return (current_mtime, current_mtime > previous_mtime)
649
+
650
+
639
651
  def _mpt_aspect_ratio(value: Any, format_value: Any = "") -> str:
640
652
  raw = str(value or format_value or "").strip().casefold()
641
653
  if raw in {"landscape 16:9", "wide", "16:9", "landscape"}:
@@ -763,6 +775,11 @@ def _moneyprinter_cli_args(task: dict[str, Any], route: str, settings: dict[str,
763
775
  clip_duration = generation_settings.get("maximum_clip_duration")
764
776
  if str(clip_duration or "").strip().isdigit() and int(clip_duration) > 0:
765
777
  args.extend(["--video-clip-duration", str(int(clip_duration))])
778
+ elif route in {"pexels", "pixabay"} and len(script) >= 1_200:
779
+ # Vídeos longos com o padrão de 5 s podem exigir dezenas de downloads
780
+ # sequenciais. Para manter variedade e terminar de forma previsível,
781
+ # adoptamos 15 s apenas quando não há escolha explícita do utilizador.
782
+ args.extend(["--video-clip-duration", "15"])
766
783
  if bool(generation_settings.get("match_visuals_to_script_order")):
767
784
  args.append("--match-materials-to-script")
768
785
 
@@ -915,6 +932,9 @@ def _run_video_helper(task: dict[str, Any]) -> Path:
915
932
  output_finished = False
916
933
  last_heartbeat = 0.0
917
934
  last_output_line = ""
935
+ last_activity_at = started_at
936
+ helper_log_path: Path | None = None
937
+ helper_log_mtime = 0.0
918
938
  try:
919
939
  while True:
920
940
  try:
@@ -924,9 +944,19 @@ def _run_video_helper(task: dict[str, Any]) -> Path:
924
944
  elif line:
925
945
  last_output_line = _redact_helper_output(line).strip()
926
946
  output_lines.append(line)
947
+ last_activity_at = time.monotonic()
948
+ match = re.search(r"full generation log:\s*(.+)$", last_output_line, flags=re.IGNORECASE)
949
+ if match:
950
+ candidate = Path(match.group(1).strip()).expanduser()
951
+ helper_log_path = candidate if candidate.is_file() else None
927
952
  except queue.Empty:
928
953
  pass
929
954
  elapsed = time.monotonic() - started_at
955
+ helper_log_mtime, helper_log_advanced = _latest_helper_log_activity(helper_log_path, helper_log_mtime)
956
+ if helper_log_advanced:
957
+ last_activity_at = time.monotonic()
958
+ if not last_output_line:
959
+ last_output_line = "[MoneyPrinterTurbo] actividade de geração confirmada"
930
960
  if elapsed - last_heartbeat >= 5:
931
961
  # O helper expõe o resultado final, mas não uma percentagem estável.
932
962
  # Mantemos uma faixa reservada para a etapa de vídeo e avançamos-a
@@ -958,6 +988,14 @@ def _run_video_helper(task: dict[str, Any]) -> Path:
958
988
  message = f"A etapa Vídeo excedeu o limite de {timeout_seconds // 60} minutos e foi encerrada."
959
989
  metadata = _failure_attribution(task, settings, "video", error=message)
960
990
  raise PipelineError(_failure_message(message, metadata), failure_metadata=metadata)
991
+ if time.monotonic() - last_activity_at >= VIDEO_IDLE_TIMEOUT_SECONDS:
992
+ _stop_process(process)
993
+ message = (
994
+ "A etapa Vídeo não apresentou actividade comprovada do motor durante "
995
+ f"{VIDEO_IDLE_TIMEOUT_SECONDS // 60} minutos e foi encerrada."
996
+ )
997
+ metadata = _failure_attribution(task, settings, "video", error=message)
998
+ raise PipelineError(_failure_message(message, metadata), failure_metadata=metadata)
961
999
  finally:
962
1000
  reader.join(timeout=2)
963
1001
  _persist_video_diagnostics(task, "\n".join(output_lines))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.3.83",
3
+ "version": "0.3.85",
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",