mirror of
https://github.com/denizsafak/abogen.git
synced 2026-09-20 11:40:57 +02:00
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:
@@ -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",
|
||||||
|
]
|
||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user