From 146cc81271d4cd3820cc04774a8f7764d9635b4e Mon Sep 17 00:00:00 2001 From: Artem Akymenko Date: Tue, 28 Jul 2026 08:04:34 +0000 Subject: [PATCH] refactor: centralize cleanup in app layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- abogen/application/cleanup.py | 73 ++++++++++++++++++++ abogen/application/conversion_service.py | 3 + abogen/shutdown.py | 88 ++++++++++-------------- abogen/webui/conversion_runner.py | 9 --- 4 files changed, 112 insertions(+), 61 deletions(-) create mode 100644 abogen/application/cleanup.py diff --git a/abogen/application/cleanup.py b/abogen/application/cleanup.py new file mode 100644 index 0000000..fa22cc2 --- /dev/null +++ b/abogen/application/cleanup.py @@ -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", +] diff --git a/abogen/application/conversion_service.py b/abogen/application/conversion_service.py index 95f3eb9..da0cd3e 100644 --- a/abogen/application/conversion_service.py +++ b/abogen/application/conversion_service.py @@ -104,6 +104,9 @@ def run_conversion( raise finally: pool.dispose_all() + voice_cache.clear() + from abogen.application.cleanup import flush_cuda + flush_cuda() def _create_voice_resolver( diff --git a/abogen/shutdown.py b/abogen/shutdown.py index 8ab078b..60e0ca5 100644 --- a/abogen/shutdown.py +++ b/abogen/shutdown.py @@ -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 import atexit -import gc import signal import sys from typing import Callable @@ -28,20 +39,11 @@ def _run_cleanups() -> None: 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) - -# 2. Shutdown web UI ConversionService -def _shutdown_conversion_service() -> None: +def _stop_conversion_service() -> None: + """Stop WebUI ConversionService worker thread.""" try: from abogen.webui.service import get_service svc = get_service() @@ -50,50 +52,18 @@ def _shutdown_conversion_service() -> None: except Exception: pass -register_cleanup(_shutdown_conversion_service) -# 3. Clear TTS pipelines and GPU memory -def _cleanup_tts_pipelines() -> None: - # Clear web UI pipeline cache +def _restore_sleep() -> None: + """Restore system sleep prevention (caffeinate/systemd-inhibit/Windows).""" try: - from abogen.webui.conversion_runner import _PIPELINES - _PIPELINES.clear() + from abogen.utils import prevent_sleep_end + prevent_sleep_end() except Exception: 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: + """Terminate all child processes (ffmpeg, etc.).""" try: import psutil except Exception: @@ -115,6 +85,20 @@ def _terminate_subprocesses() -> None: except Exception: 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) @@ -133,7 +117,7 @@ def register_shutdown() -> None: except Exception: pass - # Qt hook + # Qt hook — connect AFTER QApplication is created try: from PyQt6.QtWidgets import QApplication diff --git a/abogen/webui/conversion_runner.py b/abogen/webui/conversion_runner.py index eb24321..5399c44 100644 --- a/abogen/webui/conversion_runner.py +++ b/abogen/webui/conversion_runner.py @@ -15,7 +15,6 @@ Engine converts Language → its own format internally. from __future__ import annotations -import gc from pathlib import Path from typing import Any @@ -191,11 +190,3 @@ def run_conversion_job(job: Job) -> None: job.error = str(exc) job.status = JobStatus.FAILED 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