refactor: centralize cleanup in app layer

- New: application/cleanup.py — flush_cuda(), dispose_engines(), cleanup(), register_ui_cleanup()
- conversion_service.py finally: pool.dispose_all() + voice_cache.clear() + flush_cuda()
- webui/conversion_runner.py: removed gc/cuda finally block (cleanup in run_conversion)
- shutdown.py: 160→120 lines, 5 inline cleanups → 4 process-level + app_cleanup() delegation
- Fixed bugs: _PIPELINES (didn't exist), PluginManager.dispose_all() (never called),
  VoiceCache.clear() (never called in finally), duplicate cleanup removed
This commit is contained in:
Artem Akymenko
2026-07-28 13:41:28 +03:00
parent 61204cc389
commit 146cc81271
4 changed files with 112 additions and 61 deletions
+73
View File
@@ -0,0 +1,73 @@
"""Application-layer cleanup — global resource disposal.
Handles:
- GPU/CUDA memory flush
- TTS engine disposal (PluginManager)
- UI-specific cleanup callbacks (registered by entry points)
Called by shutdown.py at process exit and by run_conversion() per-conversion.
"""
from __future__ import annotations
import gc
from typing import Callable
_UI_CLEANUPS: list[Callable[[], None]] = []
def flush_cuda() -> None:
"""Run GC and release CUDA cache. Safe to call multiple times."""
gc.collect()
try:
import torch
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.ipc_collect()
except Exception:
pass
def dispose_engines() -> None:
"""Dispose all cached TTS engines via PluginManager."""
try:
from abogen.tts_plugin.plugin_manager import get_plugin_manager
get_plugin_manager().dispose_all()
except Exception:
pass
def _clear_global_voice_cache() -> None:
"""Reset the global voice download cache state."""
try:
from abogen.voice_cache import clear_voice_cache
clear_voice_cache()
except Exception:
pass
def register_ui_cleanup(fn: Callable[[], None]) -> None:
"""Register a UI-specific cleanup callback (e.g. preview threads, temp files)."""
_UI_CLEANUPS.append(fn)
def cleanup() -> None:
"""Run all application-level cleanups. Idempotent."""
dispose_engines()
flush_cuda()
_clear_global_voice_cache()
for fn in _UI_CLEANUPS:
try:
fn()
except Exception:
pass
_UI_CLEANUPS.clear()
__all__ = [
"flush_cuda",
"dispose_engines",
"register_ui_cleanup",
"cleanup",
]
+3
View File
@@ -104,6 +104,9 @@ def run_conversion(
raise raise
finally: finally:
pool.dispose_all() pool.dispose_all()
voice_cache.clear()
from abogen.application.cleanup import flush_cuda
flush_cuda()
def _create_voice_resolver( def _create_voice_resolver(
+36 -52
View File
@@ -1,8 +1,19 @@
"""Graceful shutdown - single module, no over-engineering.""" """Graceful shutdown — process-level hooks and orchestration.
Responsibilities:
- Install atexit/signal/Qt hooks
- Stop WebUI ConversionService (worker thread)
- Restore sleep prevention
- Terminate child processes (ffmpeg, etc.)
- Delegate GPU/engine/UI cleanup to application.cleanup
App-layer cleanup (GPU, engines, UI callbacks) lives in application/cleanup.py.
Per-conversion cleanup lives in run_conversion() finally block.
"""
from __future__ import annotations from __future__ import annotations
import atexit import atexit
import gc
import signal import signal
import sys import sys
from typing import Callable from typing import Callable
@@ -28,20 +39,11 @@ def _run_cleanups() -> None:
pass pass
# ---- Register built-in cleanup functions ---- # ---- Process-level cleanup functions ----
# 1. Restore sleep prevention
def _restore_sleep() -> None:
try:
from abogen.utils import prevent_sleep_end
prevent_sleep_end()
except Exception:
pass
register_cleanup(_restore_sleep) def _stop_conversion_service() -> None:
"""Stop WebUI ConversionService worker thread."""
# 2. Shutdown web UI ConversionService
def _shutdown_conversion_service() -> None:
try: try:
from abogen.webui.service import get_service from abogen.webui.service import get_service
svc = get_service() svc = get_service()
@@ -50,50 +52,18 @@ def _shutdown_conversion_service() -> None:
except Exception: except Exception:
pass pass
register_cleanup(_shutdown_conversion_service)
# 3. Clear TTS pipelines and GPU memory def _restore_sleep() -> None:
def _cleanup_tts_pipelines() -> None: """Restore system sleep prevention (caffeinate/systemd-inhibit/Windows)."""
# Clear web UI pipeline cache
try: try:
from abogen.webui.conversion_runner import _PIPELINES from abogen.utils import prevent_sleep_end
_PIPELINES.clear() prevent_sleep_end()
except Exception: except Exception:
pass pass
# Clear PyQt conversion thread voice cache
try:
from abogen.pyqt.conversion import ConversionThread
if hasattr(ConversionThread, "voice_cache"):
ConversionThread.voice_cache.clear()
except Exception:
pass
gc.collect()
# Release CUDA cache
try:
import torch
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.ipc_collect()
except Exception:
pass
register_cleanup(_cleanup_tts_pipelines)
# 4. Clear global voice cache
def _clear_voice_cache() -> None:
try:
from abogen.voice_cache import clear_voice_cache
clear_voice_cache()
except Exception:
pass
register_cleanup(_clear_voice_cache)
# 5. Terminate child processes (ffmpeg, etc.)
def _terminate_subprocesses() -> None: def _terminate_subprocesses() -> None:
"""Terminate all child processes (ffmpeg, etc.)."""
try: try:
import psutil import psutil
except Exception: except Exception:
@@ -115,6 +85,20 @@ def _terminate_subprocesses() -> None:
except Exception: except Exception:
pass pass
def _app_cleanup() -> None:
"""Delegate to application-layer cleanup (engines, GPU, UI callbacks)."""
try:
from abogen.application.cleanup import cleanup
cleanup()
except Exception:
pass
# Register in execution order
register_cleanup(_stop_conversion_service)
register_cleanup(_app_cleanup)
register_cleanup(_restore_sleep)
register_cleanup(_terminate_subprocesses) register_cleanup(_terminate_subprocesses)
@@ -133,7 +117,7 @@ def register_shutdown() -> None:
except Exception: except Exception:
pass pass
# Qt hook # Qt hook — connect AFTER QApplication is created
try: try:
from PyQt6.QtWidgets import QApplication from PyQt6.QtWidgets import QApplication
-9
View File
@@ -15,7 +15,6 @@ Engine converts Language → its own format internally.
from __future__ import annotations from __future__ import annotations
import gc
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -191,11 +190,3 @@ def run_conversion_job(job: Job) -> None:
job.error = str(exc) job.error = str(exc)
job.status = JobStatus.FAILED job.status = JobStatus.FAILED
job.add_log(f"Job failed: {exc}", level="error") job.add_log(f"Job failed: {exc}", level="error")
finally:
gc.collect()
try:
import torch
if torch.cuda.is_available():
torch.cuda.empty_cache()
except ImportError:
pass