@danhachuel/thunderbolt 0.3.82 → 0.3.83
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/hermes_ui/pipeline_worker.py +41 -5
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import json
|
|
|
4
4
|
import os
|
|
5
5
|
import queue
|
|
6
6
|
import re
|
|
7
|
+
import signal
|
|
7
8
|
import subprocess
|
|
8
9
|
import threading
|
|
9
10
|
import time
|
|
@@ -27,6 +28,7 @@ from hermes_ui.thumbnail_generation import ThumbnailGenerationError, generate_th
|
|
|
27
28
|
PIPELINE_LOCK_FILENAME = "pipeline_worker.lock"
|
|
28
29
|
PIPELINE_LOG_FILENAME = "pipeline_worker.json"
|
|
29
30
|
VIDEO_TIMEOUT_SECONDS = 20 * 60
|
|
31
|
+
LONG_STOCK_VIDEO_TIMEOUT_SECONDS = 45 * 60
|
|
30
32
|
STALE_TASK_SECONDS = VIDEO_TIMEOUT_SECONDS + 5 * 60
|
|
31
33
|
WORKER_HEARTBEAT_TIMEOUT_SECONDS = 15
|
|
32
34
|
CASCADE_STAGE_ORDER = ("topic", "script", "title", "keywords", "video", "thumbnail_prompt", "thumbnail", "upload")
|
|
@@ -184,13 +186,14 @@ def _recover_stale_tasks() -> list[str]:
|
|
|
184
186
|
if not updated_at:
|
|
185
187
|
continue
|
|
186
188
|
age_seconds = (current_time - updated_at.astimezone(timezone.utc)).total_seconds()
|
|
187
|
-
|
|
189
|
+
timeout_seconds = _task_stale_timeout_seconds(task)
|
|
190
|
+
if age_seconds <= timeout_seconds:
|
|
188
191
|
continue
|
|
189
192
|
task_id = str(task.get("id") or "")
|
|
190
193
|
if not task_id:
|
|
191
194
|
continue
|
|
192
195
|
message = (
|
|
193
|
-
f"A tarefa ficou sem heartbeat durante mais de {
|
|
196
|
+
f"A tarefa ficou sem heartbeat durante mais de {timeout_seconds // 60} minutos. "
|
|
194
197
|
"Foi marcada como falhada para evitar execução eterna; reveja o log do worker."
|
|
195
198
|
)
|
|
196
199
|
failed_stage = str(task.get("stage") or "pipeline")
|
|
@@ -568,7 +571,21 @@ def _persist_video_diagnostics(task: dict[str, Any], output: str) -> dict[str, s
|
|
|
568
571
|
|
|
569
572
|
def _stop_process(process: subprocess.Popen[str]) -> None:
|
|
570
573
|
if process.poll() is None:
|
|
571
|
-
|
|
574
|
+
try:
|
|
575
|
+
process_id = getattr(process, "pid", None)
|
|
576
|
+
if os.name != "nt" and process_id:
|
|
577
|
+
os.killpg(os.getpgid(process_id), signal.SIGKILL)
|
|
578
|
+
elif os.name == "nt" and process_id:
|
|
579
|
+
subprocess.run(
|
|
580
|
+
["taskkill", "/PID", str(process_id), "/T", "/F"],
|
|
581
|
+
check=False,
|
|
582
|
+
stdout=subprocess.DEVNULL,
|
|
583
|
+
stderr=subprocess.DEVNULL,
|
|
584
|
+
)
|
|
585
|
+
else:
|
|
586
|
+
process.kill()
|
|
587
|
+
except (OSError, ProcessLookupError):
|
|
588
|
+
process.kill()
|
|
572
589
|
try:
|
|
573
590
|
process.wait(timeout=5)
|
|
574
591
|
except subprocess.TimeoutExpired:
|
|
@@ -602,6 +619,23 @@ def _normalise_video_route(task: dict[str, Any], settings: dict[str, Any]) -> st
|
|
|
602
619
|
return raw if raw in {"pexels", "pixabay", "local"} else "pexels"
|
|
603
620
|
|
|
604
621
|
|
|
622
|
+
def _video_timeout_seconds(task: dict[str, Any], settings: dict[str, Any] | None = None) -> int:
|
|
623
|
+
"""Reserve extra bounded time only for long stock-video downloads and assembly."""
|
|
624
|
+
effective_settings = settings if isinstance(settings, dict) else _settings()
|
|
625
|
+
route = _normalise_video_route(task, effective_settings)
|
|
626
|
+
script = str(task.get("video_script") or "").strip()
|
|
627
|
+
if route in {"pexels", "pixabay"} and len(script) >= 1_200:
|
|
628
|
+
return max(VIDEO_TIMEOUT_SECONDS, LONG_STOCK_VIDEO_TIMEOUT_SECONDS)
|
|
629
|
+
return VIDEO_TIMEOUT_SECONDS
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _task_stale_timeout_seconds(task: dict[str, Any]) -> int:
|
|
633
|
+
"""Keep stale-task recovery aligned with the actual execution budget."""
|
|
634
|
+
if str(task.get("stage") or "").strip().casefold() == "video":
|
|
635
|
+
return _video_timeout_seconds(task) + 5 * 60
|
|
636
|
+
return STALE_TASK_SECONDS
|
|
637
|
+
|
|
638
|
+
|
|
605
639
|
def _mpt_aspect_ratio(value: Any, format_value: Any = "") -> str:
|
|
606
640
|
raw = str(value or format_value or "").strip().casefold()
|
|
607
641
|
if raw in {"landscape 16:9", "wide", "16:9", "landscape"}:
|
|
@@ -847,6 +881,7 @@ def _run_video_helper(task: dict[str, Any]) -> Path:
|
|
|
847
881
|
output_lines: list[str] = []
|
|
848
882
|
line_queue: queue.Queue[str | None] = queue.Queue()
|
|
849
883
|
started_at = time.monotonic()
|
|
884
|
+
timeout_seconds = _video_timeout_seconds(task, settings)
|
|
850
885
|
process: subprocess.Popen[str] | None = None
|
|
851
886
|
|
|
852
887
|
def _read_output() -> None:
|
|
@@ -863,6 +898,7 @@ def _run_video_helper(task: dict[str, Any]) -> Path:
|
|
|
863
898
|
command,
|
|
864
899
|
cwd=helper_dir,
|
|
865
900
|
env=env,
|
|
901
|
+
start_new_session=os.name != "nt",
|
|
866
902
|
stdout=subprocess.PIPE,
|
|
867
903
|
stderr=subprocess.STDOUT,
|
|
868
904
|
text=True,
|
|
@@ -917,9 +953,9 @@ def _run_video_helper(task: dict[str, Any]) -> Path:
|
|
|
917
953
|
last_heartbeat = elapsed
|
|
918
954
|
if process.poll() is not None and output_finished:
|
|
919
955
|
break
|
|
920
|
-
if elapsed >=
|
|
956
|
+
if elapsed >= timeout_seconds:
|
|
921
957
|
_stop_process(process)
|
|
922
|
-
message = f"A etapa Vídeo excedeu o limite de {
|
|
958
|
+
message = f"A etapa Vídeo excedeu o limite de {timeout_seconds // 60} minutos e foi encerrada."
|
|
923
959
|
metadata = _failure_attribution(task, settings, "video", error=message)
|
|
924
960
|
raise PipelineError(_failure_message(message, metadata), failure_metadata=metadata)
|
|
925
961
|
finally:
|
package/package.json
CHANGED