mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
155 lines
4.3 KiB
Python
155 lines
4.3 KiB
Python
from __future__ import annotations
|
||
|
||
import os
|
||
import threading
|
||
from typing import Callable, Dict, Iterable, Optional, Set, Tuple
|
||
|
||
try: # pragma: no cover - optional dependency guard
|
||
from huggingface_hub import hf_hub_download # type: ignore
|
||
from huggingface_hub.utils import LocalEntryNotFoundError # type: ignore
|
||
except Exception: # pragma: no cover - import fallback
|
||
hf_hub_download = None # type: ignore[assignment]
|
||
LocalEntryNotFoundError = None # type: ignore[assignment]
|
||
|
||
if LocalEntryNotFoundError is None: # pragma: no cover - fallback for tests
|
||
|
||
class LocalEntryNotFoundError(Exception):
|
||
pass
|
||
|
||
|
||
from abogen.tts_plugin.utils import get_voices
|
||
|
||
_CACHE_LOCK = threading.Lock()
|
||
_CACHED_VOICES: Set[str] = set()
|
||
_BOOTSTRAP_LOCK = threading.Lock()
|
||
_BOOTSTRAPPED = False
|
||
|
||
|
||
def _normalize_targets(voices: Optional[Iterable[str]]) -> Set[str]:
|
||
kokoro_voices = get_voices("kokoro")
|
||
if not voices:
|
||
return set(kokoro_voices)
|
||
normalized: Set[str] = set()
|
||
for voice in voices:
|
||
if not voice:
|
||
continue
|
||
voice_id = str(voice).strip()
|
||
if not voice_id:
|
||
continue
|
||
if voice_id in kokoro_voices:
|
||
normalized.add(voice_id)
|
||
return normalized
|
||
|
||
|
||
def ensure_voice_assets(
|
||
voices: Optional[Iterable[str]] = None,
|
||
*,
|
||
repo_id: str = "hexgrad/Kokoro-82M",
|
||
cache_dir: Optional[str] = None,
|
||
on_progress: Optional[Callable[[str], None]] = None,
|
||
) -> Tuple[Set[str], Dict[str, str]]:
|
||
"""Ensure Kokoro voice weight files are present locally.
|
||
|
||
Returns a tuple of (downloaded voices, errors) where errors maps the
|
||
voice id to the underlying exception message.
|
||
"""
|
||
|
||
if hf_hub_download is None:
|
||
raise RuntimeError("huggingface_hub is required to cache voices")
|
||
|
||
effective_cache_dir = cache_dir
|
||
if effective_cache_dir is None:
|
||
env_cache_dir = os.environ.get("ABOGEN_VOICE_CACHE_DIR", "").strip()
|
||
effective_cache_dir = env_cache_dir or None
|
||
|
||
targets = _normalize_targets(voices)
|
||
if not targets:
|
||
return set(), {}
|
||
|
||
with _CACHE_LOCK:
|
||
missing = [voice for voice in targets if voice not in _CACHED_VOICES]
|
||
|
||
downloaded: Set[str] = set()
|
||
errors: Dict[str, str] = {}
|
||
|
||
for voice_id in missing:
|
||
if on_progress:
|
||
on_progress(f"Fetching voice asset '{voice_id}'")
|
||
try:
|
||
downloaded_flag = _ensure_single_voice_asset(
|
||
voice_id,
|
||
repo_id=repo_id,
|
||
cache_dir=effective_cache_dir,
|
||
)
|
||
except Exception as exc: # pragma: no cover - network variance
|
||
errors[voice_id] = str(exc)
|
||
continue
|
||
|
||
if downloaded_flag:
|
||
downloaded.add(voice_id)
|
||
with _CACHE_LOCK:
|
||
_CACHED_VOICES.add(voice_id)
|
||
|
||
return downloaded, errors
|
||
|
||
|
||
def bootstrap_voice_cache(
|
||
voices: Optional[Iterable[str]] = None,
|
||
*,
|
||
repo_id: str = "hexgrad/Kokoro-82M",
|
||
cache_dir: Optional[str] = None,
|
||
on_progress: Optional[Callable[[str], None]] = None,
|
||
) -> Tuple[Set[str], Dict[str, str]]:
|
||
"""Ensure voices are cached once per process.
|
||
|
||
Subsequent calls are no-ops and return empty structures.
|
||
"""
|
||
|
||
global _BOOTSTRAPPED
|
||
with _BOOTSTRAP_LOCK:
|
||
if _BOOTSTRAPPED:
|
||
return set(), {}
|
||
downloaded, errors = ensure_voice_assets(
|
||
voices,
|
||
repo_id=repo_id,
|
||
cache_dir=cache_dir,
|
||
on_progress=on_progress,
|
||
)
|
||
_BOOTSTRAPPED = True
|
||
return downloaded, errors
|
||
|
||
|
||
def _ensure_single_voice_asset(
|
||
voice_id: str,
|
||
*,
|
||
repo_id: str,
|
||
cache_dir: Optional[str],
|
||
) -> bool:
|
||
if hf_hub_download is None:
|
||
raise RuntimeError("huggingface_hub is required to cache voices")
|
||
|
||
filename = f"voices/{voice_id}.pt"
|
||
common_kwargs = {
|
||
"repo_id": repo_id,
|
||
"filename": filename,
|
||
}
|
||
if cache_dir is not None:
|
||
common_kwargs["cache_dir"] = cache_dir
|
||
|
||
try:
|
||
hf_hub_download(local_files_only=True, **common_kwargs)
|
||
return False
|
||
except LocalEntryNotFoundError:
|
||
pass
|
||
|
||
hf_hub_download(resume_download=True, **common_kwargs)
|
||
return True
|
||
|
||
|
||
def clear_voice_cache() -> None:
|
||
"""Clear the in‑process voice cache (used during shutdown)."""
|
||
with _CACHE_LOCK:
|
||
_CACHED_VOICES.clear()
|
||
global _BOOTSTRAPPED
|
||
_BOOTSTRAPPED = False
|