165 lines
6.1 KiB
Python
165 lines
6.1 KiB
Python
import os
|
|
import sys
|
|
import gc
|
|
import torch
|
|
from typing import Optional, Dict, Any
|
|
|
|
|
|
class IndexTTS2Loader:
|
|
"""
|
|
Lightweight model manager for IndexTTS2.
|
|
- Resolves model root: <ComfyUI>/models/IndexTTS-2
|
|
- Validates required files
|
|
- Lazy loads submodules and caches them
|
|
"""
|
|
|
|
DEFAULT_DIRNAME = "IndexTTS-2"
|
|
|
|
def __init__(self, models_root: Optional[str] = None, device: Optional[str] = None, dtype: Optional[str] = None):
|
|
self._models_root = models_root or self._default_models_root()
|
|
self._model_dir = os.path.join(self._models_root, self.DEFAULT_DIRNAME)
|
|
self._device = torch.device(device) if device else (torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu"))
|
|
# Default to fp16 when running on CUDA to reduce VRAM and speed up inference; stay fp32 on CPU
|
|
if dtype is None and self._device.type == "cuda":
|
|
dtype = "fp16"
|
|
self._dtype = self._resolve_dtype(dtype)
|
|
self._cache: Dict[str, Any] = {}
|
|
# Prepare path to vendored IndexTTS2 source: indextts2/vendor/indextts
|
|
self._vendor_root = os.path.join(os.path.dirname(os.path.abspath(__file__)), "vendor")
|
|
self._vendor_pkg_root = os.path.join(self._vendor_root, "indextts")
|
|
if os.path.isdir(self._vendor_root):
|
|
try:
|
|
sys.path.remove(self._vendor_root)
|
|
except ValueError:
|
|
pass
|
|
sys.path.insert(0, self._vendor_root)
|
|
|
|
@staticmethod
|
|
def _default_models_root() -> str:
|
|
# <repo>/ComfyUI/models
|
|
# This file is at: .../ComfyUI/custom_nodes/ComfyUI-Index-TTS/indextts2/model_loader.py
|
|
# Go up 4 levels to reach .../ComfyUI/
|
|
here = os.path.abspath(__file__)
|
|
for _ in range(4):
|
|
here = os.path.dirname(here)
|
|
return os.path.join(here, "models")
|
|
|
|
@staticmethod
|
|
def _resolve_dtype(dtype: Optional[str]):
|
|
if isinstance(dtype, torch.dtype):
|
|
return dtype
|
|
if dtype == "fp16":
|
|
return torch.float16
|
|
if dtype == "bf16":
|
|
return torch.bfloat16
|
|
return torch.float32
|
|
|
|
@property
|
|
def device(self):
|
|
return self._device
|
|
|
|
@property
|
|
def dtype(self):
|
|
return self._dtype
|
|
|
|
@property
|
|
def model_dir(self):
|
|
return self._model_dir
|
|
|
|
def validate(self) -> None:
|
|
required = [
|
|
"bpe.model",
|
|
"config.yaml",
|
|
"feat1.pt",
|
|
"feat2.pt",
|
|
"gpt.pth",
|
|
"s2mel.pth",
|
|
"wav2vec2bert_stats.pt",
|
|
]
|
|
missing = [f for f in required if not os.path.exists(os.path.join(self._model_dir, f))]
|
|
if missing:
|
|
raise FileNotFoundError(f"IndexTTS-2 missing files in {self._model_dir}: {', '.join(missing)}")
|
|
|
|
def get_components(self) -> Dict[str, Any]:
|
|
"""
|
|
Placeholder that returns a dict-like components container.
|
|
Later this will actually initialize models from 原项目代码/index-tts.
|
|
"""
|
|
if "components" not in self._cache:
|
|
# Lazy container; real impl will import and construct modules
|
|
self._cache["components"] = {
|
|
"status": "placeholder",
|
|
"model_dir": self._model_dir,
|
|
"device": str(self._device),
|
|
"dtype": str(self._dtype),
|
|
}
|
|
return self._cache["components"]
|
|
|
|
def get_tts(self):
|
|
"""
|
|
Return a cached instance of indextts.infer_v2.IndexTTS2 constructed with our model_dir.
|
|
"""
|
|
if "tts" in self._cache:
|
|
return self._cache["tts"]
|
|
|
|
# Ensure validation before heavy init for clearer error
|
|
self.validate()
|
|
|
|
try:
|
|
# Purge any previously imported legacy 'indextts' to avoid shadowing
|
|
for k in list(sys.modules.keys()):
|
|
if k == "indextts" or k.startswith("indextts."):
|
|
sys.modules.pop(k, None)
|
|
from .vendor.indextts.infer_v2 import IndexTTS2 # imported from vendored package
|
|
except Exception as e:
|
|
# Fallback: import by file path to avoid package name collisions
|
|
try:
|
|
import importlib.util
|
|
infer_path = os.path.join(self._vendor_pkg_root, "infer_v2.py")
|
|
spec = importlib.util.spec_from_file_location("indextts_infer_v2", infer_path)
|
|
if spec is None or spec.loader is None:
|
|
raise ImportError(f"spec load failed for {infer_path}")
|
|
mod = importlib.util.module_from_spec(spec)
|
|
sys.modules["indextts_infer_v2"] = mod
|
|
spec.loader.exec_module(mod)
|
|
IndexTTS2 = getattr(mod, "IndexTTS2")
|
|
except Exception as e2:
|
|
raise ImportError(
|
|
f"Failed to import IndexTTS2 from vendored source at {self._vendor_pkg_root}. Error: {e}. Fallback failed: {e2}. "
|
|
"Ensure project dependencies (transformers, modelscope, huggingface_hub, torchaudio, safetensors, omegaconf, etc.) are installed."
|
|
)
|
|
|
|
cfg_path = os.path.join(self._model_dir, "config.yaml")
|
|
# Upstream IndexTTS2 expects 'is_fp16' (HF demo), not 'use_fp16'
|
|
tts = IndexTTS2(
|
|
cfg_path=cfg_path,
|
|
model_dir=self._model_dir,
|
|
is_fp16=(self._dtype == torch.float16),
|
|
device=str(self._device),
|
|
)
|
|
self._cache["tts"] = tts
|
|
return tts
|
|
|
|
def unload_tts(self) -> None:
|
|
"""
|
|
Best-effort unload of cached TTS instance and free GPU cache to reduce VRAM.
|
|
Safe to call even if not loaded.
|
|
"""
|
|
try:
|
|
tts = self._cache.pop("tts", None)
|
|
# Break potential cyc refs early
|
|
del tts
|
|
except Exception:
|
|
pass
|
|
# Collect Python garbage and free CUDA cache
|
|
try:
|
|
gc.collect()
|
|
except Exception:
|
|
pass
|
|
try:
|
|
if torch.cuda.is_available():
|
|
torch.cuda.synchronize()
|
|
torch.cuda.empty_cache()
|
|
except Exception:
|
|
pass
|