2066 lines
90 KiB
Python
2066 lines
90 KiB
Python
"""
|
|
Unified Model Interface for TTS Audio Suite
|
|
|
|
This module provides a standardized interface for all TTS engines to use
|
|
ComfyUI's model management system. All engines (ChatterBox, F5-TTS, Higgs Audio,
|
|
RVC, etc.) should use this interface instead of direct caching.
|
|
"""
|
|
|
|
import torch
|
|
from typing import Any, Dict, Optional, Callable, Union
|
|
from pathlib import Path
|
|
|
|
from utils.models.comfyui_model_wrapper import tts_model_manager, ModelInfo
|
|
from utils.models.factory_config import ModelLoadConfig, runtime_uses_isolation
|
|
from utils.models.engine_registry import get_default_runtime_profile
|
|
from utils.runtimes import (
|
|
build_fish_audio_s2_proxy,
|
|
build_higgs_audio_isolated_proxy,
|
|
build_qwen3_asr_isolated_proxy,
|
|
build_qwen3_tts_isolated_proxy,
|
|
build_step_audio_editx_isolated_proxy,
|
|
build_vibevoice_isolated_proxy,
|
|
)
|
|
from utils.models.comfyui_model_wrapper.cache_utils import invalidate_all_caches
|
|
|
|
|
|
class UnifiedModelInterface:
|
|
"""
|
|
Unified interface for all TTS engine model loading.
|
|
|
|
This replaces all engine-specific caching systems with a single
|
|
ComfyUI-integrated approach.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize the unified interface"""
|
|
self._model_factories: Dict[str, Callable] = {}
|
|
self._pytorch_warning_shown = False
|
|
self._isolated_model_cache: Dict[str, Any] = {}
|
|
|
|
def register_model_factory(self,
|
|
engine_name: str,
|
|
model_type: str,
|
|
factory_func: Callable) -> None:
|
|
"""
|
|
Register a model factory function for an engine.
|
|
|
|
Args:
|
|
engine_name: Name of the engine ("chatterbox", "f5tts", etc.)
|
|
model_type: Type of model ("tts", "vc", "tokenizer", etc.)
|
|
factory_func: Function that creates the model
|
|
"""
|
|
key = f"{engine_name}_{model_type}"
|
|
self._model_factories[key] = factory_func
|
|
|
|
def _check_pytorch_consistency(self) -> None:
|
|
"""Check for PyTorch/TorchAudio/TorchVision consistency and warn if mixed CUDA/CPU"""
|
|
if self._pytorch_warning_shown:
|
|
return
|
|
|
|
try:
|
|
import torch
|
|
import torchaudio
|
|
import torchvision
|
|
|
|
# Check PyTorch version info
|
|
torch_version = getattr(torch, '__version__', 'unknown')
|
|
torchaudio_version = getattr(torchaudio, '__version__', 'unknown')
|
|
torchvision_version = getattr(torchvision, '__version__', 'unknown')
|
|
|
|
# Detect CUDA vs CPU variants
|
|
torch_cuda = '+cu' in torch_version
|
|
torch_cpu = '+cpu' in torch_version
|
|
torchaudio_cpu = '+cpu' in torchaudio_version or (not '+cu' in torchaudio_version and not torch_cpu)
|
|
torchvision_cpu = '+cpu' in torchvision_version or (not '+cu' in torchvision_version and not torch_cpu)
|
|
|
|
warnings = []
|
|
|
|
# Check for performance-killing mixed installations
|
|
if torch_cuda and torchaudio_cpu:
|
|
warnings.append("❌ CRITICAL PERFORMANCE WARNING: GPU PyTorch + CPU TorchAudio detected!")
|
|
warnings.append(f" PyTorch: {torch_version} | TorchAudio: {torchaudio_version}")
|
|
warnings.append(" This causes major slowdowns and VRAM spikes in TTS generation.")
|
|
warnings.append(" Fix: pip uninstall torchaudio && pip install torchaudio --index-url https://download.pytorch.org/whl/cu129")
|
|
|
|
if torch_cuda and torchvision_cpu:
|
|
warnings.append("❌ PERFORMANCE WARNING: GPU PyTorch + CPU TorchVision detected!")
|
|
warnings.append(f" PyTorch: {torch_version} | TorchVision: {torchvision_version}")
|
|
warnings.append(" Fix: pip uninstall torchvision && pip install torchvision --index-url https://download.pytorch.org/whl/cu129")
|
|
|
|
if warnings:
|
|
print("\n" + "="*80)
|
|
print("🔥 TTS AUDIO SUITE - PYTORCH INSTALLATION WARNING")
|
|
print("="*80)
|
|
for warning in warnings:
|
|
print(warning)
|
|
print("="*80 + "\n")
|
|
|
|
self._pytorch_warning_shown = True
|
|
|
|
except ImportError as e:
|
|
# PyTorch not available - this will be caught elsewhere
|
|
pass
|
|
except Exception as e:
|
|
# Don't let consistency check break model loading
|
|
print(f"⚠️ PyTorch consistency check failed: {e}")
|
|
self._pytorch_warning_shown = True
|
|
|
|
def load_model(self, config: ModelLoadConfig, force_reload: bool = False) -> Any:
|
|
"""
|
|
Load a model using the unified interface.
|
|
|
|
Args:
|
|
config: Model configuration (ModelLoadConfig)
|
|
force_reload: Force reload even if cached
|
|
|
|
Returns:
|
|
The loaded model (unwrapped from ComfyUI wrapper)
|
|
|
|
Raises:
|
|
ValueError: If no factory is registered for the engine/model type
|
|
RuntimeError: If model loading fails
|
|
"""
|
|
# Check PyTorch consistency on first model load
|
|
self._check_pytorch_consistency()
|
|
|
|
if runtime_uses_isolation(config.runtime_mode):
|
|
return self._load_isolated_model(config, force_reload=force_reload)
|
|
|
|
self._clear_conflicting_isolated_models(config)
|
|
|
|
# Apply transformers compatibility patches
|
|
try:
|
|
import __init__ as tts_suite_init
|
|
if hasattr(tts_suite_init, "_apply_transformers_patches_once"):
|
|
tts_suite_init._apply_transformers_patches_once()
|
|
except ImportError:
|
|
pass
|
|
|
|
# Generate unique cache key
|
|
cache_key = self._generate_cache_key(config)
|
|
|
|
# Check if force reload is needed
|
|
if force_reload:
|
|
tts_model_manager.remove_model(cache_key)
|
|
|
|
# CRITICAL: For engines that support multiple model variants (like Qwen3-TTS),
|
|
# check if a DIFFERENT variant is already loaded and unload it to prevent device conflicts
|
|
# Only applies to engines where model variants are mutually exclusive
|
|
if config.engine_name in (
|
|
"qwen3_tts", "moss_tts", "higgs_audio_v3", "dramabox"
|
|
):
|
|
# Check for any cached mutually-exclusive model variant for this engine
|
|
cached_prefix = f"{config.engine_name}_tts_"
|
|
cached_keys = [k for k in tts_model_manager._model_cache.keys() if k.startswith(cached_prefix)]
|
|
for existing_key in cached_keys:
|
|
if existing_key != cache_key:
|
|
# Different model variant is loaded - unload it first
|
|
print(f"🗑️ Unloading old {config.engine_name} model variant to prevent VRAM accumulation")
|
|
tts_model_manager.remove_model(existing_key)
|
|
|
|
# CRITICAL: Invalidate processor caches so they reload engines
|
|
from utils.models.comfyui_model_wrapper.cache_utils import invalidate_all_caches
|
|
invalidate_all_caches()
|
|
|
|
# Get existing model if cached
|
|
wrapper = tts_model_manager.get_model(cache_key)
|
|
if wrapper is not None:
|
|
# Check if wrapper's model was deleted (e.g., by ComfyUI auto-unload for bitsandbytes models)
|
|
if hasattr(wrapper, 'is_dead') and wrapper.is_dead():
|
|
print(f"⚠️ Cached model wrapper is dead (model was deleted), forcing reload...")
|
|
tts_model_manager.remove_model(cache_key)
|
|
wrapper = None # Fall through to reload
|
|
else:
|
|
# Ensure model is on correct device
|
|
from utils.device import resolve_torch_device
|
|
target_device = resolve_torch_device(config.device)
|
|
|
|
# Resolve wrapper's current device for consistent comparison
|
|
# wrapper.current_device might be string or torch.device object
|
|
wrapper_device_resolved = resolve_torch_device(
|
|
wrapper.current_device if isinstance(wrapper.current_device, str)
|
|
else str(wrapper.current_device)
|
|
)
|
|
|
|
# Reload if device mismatch (handles both explicit devices and "auto" resolution)
|
|
if wrapper_device_resolved != target_device:
|
|
wrapper.model_load(target_device)
|
|
return wrapper
|
|
|
|
# Find appropriate factory
|
|
factory_key = f"{config.engine_name}_{config.model_type}"
|
|
if factory_key not in self._model_factories:
|
|
raise ValueError(f"No model factory registered for {factory_key}")
|
|
|
|
factory_func = self._model_factories[factory_key]
|
|
|
|
# Use ComfyUI model manager to load (pass config to factory)
|
|
wrapper = tts_model_manager.load_model(
|
|
model_factory_func=factory_func,
|
|
model_key=cache_key,
|
|
model_type=config.model_type,
|
|
engine=config.engine_name,
|
|
device=config.device,
|
|
force_reload=force_reload,
|
|
config=config # Pass entire config to factory
|
|
)
|
|
|
|
return wrapper
|
|
|
|
def _load_isolated_model(self, config: ModelLoadConfig, force_reload: bool = False) -> Any:
|
|
cache_key = self._generate_cache_key(config)
|
|
|
|
self._clear_conflicting_isolated_models(config)
|
|
self._clear_conflicting_embedded_models(config, keep_cache_key=cache_key)
|
|
|
|
if force_reload:
|
|
self._remove_isolated_model(cache_key)
|
|
|
|
cached = self._isolated_model_cache.get(cache_key)
|
|
if cached is not None:
|
|
return cached
|
|
|
|
profile_name = config.runtime_profile or get_default_runtime_profile(config.engine_name or "")
|
|
config.runtime_profile = profile_name
|
|
|
|
if config.engine_name == "vibevoice" and config.model_type == "tts":
|
|
proxy = build_vibevoice_isolated_proxy(config)
|
|
self._isolated_model_cache[cache_key] = proxy
|
|
return proxy
|
|
|
|
if config.engine_name == "higgs_audio" and config.model_type == "tts":
|
|
proxy = build_higgs_audio_isolated_proxy(config)
|
|
self._isolated_model_cache[cache_key] = proxy
|
|
return proxy
|
|
|
|
if config.engine_name == "qwen3_tts" and config.model_type == "tts":
|
|
proxy = build_qwen3_tts_isolated_proxy(config)
|
|
self._isolated_model_cache[cache_key] = proxy
|
|
return proxy
|
|
|
|
if config.engine_name == "fish_audio_s2" and config.model_type == "tts":
|
|
proxy = build_fish_audio_s2_proxy(config)
|
|
self._isolated_model_cache[cache_key] = proxy
|
|
return proxy
|
|
|
|
if config.engine_name == "qwen3_asr" and config.model_type in ("asr", "aligner"):
|
|
proxy = build_qwen3_asr_isolated_proxy(config)
|
|
self._isolated_model_cache[cache_key] = proxy
|
|
return proxy
|
|
|
|
if config.engine_name == "step_audio_editx" and config.model_type == "tts":
|
|
proxy = build_step_audio_editx_isolated_proxy(config)
|
|
self._isolated_model_cache[cache_key] = proxy
|
|
return proxy
|
|
|
|
raise RuntimeError(
|
|
f"Isolated runtime is not implemented for engine '{config.engine_name}' "
|
|
f"(model_type='{config.model_type}', profile='{profile_name or '(none)'}')"
|
|
)
|
|
|
|
def _remove_isolated_model(self, cache_key: str) -> bool:
|
|
model = self._isolated_model_cache.pop(cache_key, None)
|
|
if model is None:
|
|
return False
|
|
|
|
cleanup = getattr(model, "cleanup", None) or getattr(model, "close", None)
|
|
if callable(cleanup):
|
|
try:
|
|
cleanup()
|
|
except Exception as e:
|
|
print(f"⚠️ Failed to clean up isolated runtime model: {e}")
|
|
invalidate_all_caches()
|
|
return True
|
|
|
|
def _clear_conflicting_isolated_models(self, config: ModelLoadConfig) -> None:
|
|
if config.model_type != "tts" or not str(config.device).startswith("cuda"):
|
|
return
|
|
|
|
keep_cache_key = self._generate_cache_key(config)
|
|
keys_to_remove = [
|
|
cache_key
|
|
for cache_key in list(self._isolated_model_cache.keys())
|
|
if "_tts_" in cache_key and cache_key != keep_cache_key
|
|
]
|
|
if not keys_to_remove:
|
|
return
|
|
|
|
print(
|
|
f"🗑️ Shutting down {len(keys_to_remove)} isolated runtime TTS model(s) "
|
|
f"before loading {config.engine_name}"
|
|
)
|
|
for cache_key in keys_to_remove:
|
|
self._remove_isolated_model(cache_key)
|
|
|
|
def _clear_conflicting_embedded_models(self, config: ModelLoadConfig, keep_cache_key: Optional[str] = None) -> None:
|
|
if config.model_type != "tts" or not str(config.device).startswith("cuda"):
|
|
return
|
|
|
|
keys_to_remove = []
|
|
for cache_key, wrapper in list(tts_model_manager._model_cache.items()):
|
|
if wrapper.model_info.model_type != "tts":
|
|
continue
|
|
if keep_cache_key is not None and cache_key == keep_cache_key:
|
|
continue
|
|
keys_to_remove.append(cache_key)
|
|
|
|
if not keys_to_remove:
|
|
return
|
|
|
|
print(
|
|
f"🗑️ Unloading {len(keys_to_remove)} embedded TTS model(s) "
|
|
f"before starting isolated runtime for {config.engine_name}"
|
|
)
|
|
for cache_key in keys_to_remove:
|
|
tts_model_manager.remove_model(cache_key)
|
|
|
|
def unload_model(self, config: ModelLoadConfig) -> bool:
|
|
"""
|
|
Unload a specific model.
|
|
|
|
Args:
|
|
config: Model configuration to unload
|
|
|
|
Returns:
|
|
True if model was unloaded, False if not found
|
|
"""
|
|
cache_key = self._generate_cache_key(config)
|
|
if runtime_uses_isolation(config.runtime_mode):
|
|
return self._remove_isolated_model(cache_key)
|
|
return tts_model_manager.remove_model(cache_key)
|
|
|
|
def clear_engine_models(self, engine_name: str) -> None:
|
|
"""Clear all models for a specific engine"""
|
|
isolated_to_remove = [
|
|
key for key in list(self._isolated_model_cache.keys())
|
|
if key.startswith(f"{engine_name}_")
|
|
]
|
|
for cache_key in isolated_to_remove:
|
|
self._remove_isolated_model(cache_key)
|
|
tts_model_manager.clear_cache(engine=engine_name)
|
|
|
|
def clear_model_type(self, model_type: str) -> None:
|
|
"""Clear all models of a specific type"""
|
|
isolated_to_remove = [
|
|
key for key in list(self._isolated_model_cache.keys())
|
|
if f"_{model_type}_" in key
|
|
]
|
|
for cache_key in isolated_to_remove:
|
|
self._remove_isolated_model(cache_key)
|
|
tts_model_manager.clear_cache(model_type=model_type)
|
|
|
|
def get_model_stats(self) -> Dict[str, Any]:
|
|
"""Get statistics about loaded models"""
|
|
stats = tts_model_manager.get_stats()
|
|
stats["isolated_models"] = len(self._isolated_model_cache)
|
|
stats["isolated_model_keys"] = list(self._isolated_model_cache.keys())
|
|
return stats
|
|
|
|
def get_cached_model(self, engine_name: str, model_type: str = "tts"):
|
|
"""
|
|
Get cached model by engine name and type.
|
|
|
|
Args:
|
|
engine_name: Engine name (e.g., "vibevoice", "step_audio_editx")
|
|
model_type: Model type (default: "tts")
|
|
|
|
Returns:
|
|
Cached model wrapper or None if not found
|
|
"""
|
|
for cache_key, model in self._isolated_model_cache.items():
|
|
if cache_key.startswith(f"{engine_name}_{model_type}_"):
|
|
return model
|
|
|
|
# Search through cache for matching engine
|
|
from utils.models.manager import tts_model_manager
|
|
for cache_key in list(tts_model_manager._model_cache.keys()):
|
|
if cache_key.startswith(f"{engine_name}_{model_type}_"):
|
|
return tts_model_manager.get_model(cache_key)
|
|
return None
|
|
|
|
def _generate_cache_key(self, config: ModelLoadConfig) -> str:
|
|
"""Generate unique cache key for model configuration"""
|
|
components = [
|
|
config.engine_name,
|
|
config.model_type,
|
|
config.model_name,
|
|
config.device,
|
|
]
|
|
|
|
# CRITICAL FIX: ChatterBox Official 23-Lang is multilingual - language shouldn't affect cache
|
|
# Only single-language models should include language in the key
|
|
if config.engine_name != "chatterbox_official_23lang":
|
|
components.append(config.language or "default")
|
|
|
|
# Add path/repo info if available
|
|
if config.model_path:
|
|
components.append(f"path:{Path(config.model_path).name}")
|
|
elif config.repo_id:
|
|
components.append(f"repo:{config.repo_id}")
|
|
|
|
# Add additional_params to ensure attention_mode, quantization, etc. trigger reloads
|
|
if config.additional_params:
|
|
# Sort params for consistent cache keys
|
|
sorted_params = sorted(config.additional_params.items())
|
|
params_str = "_".join([f"{k}:{v}" for k, v in sorted_params])
|
|
components.append(f"params:{params_str}")
|
|
|
|
cache_key = "_".join(components)
|
|
# print(f"🔑 Generated cache key: {cache_key}") # Debug: verify llm_filename in key
|
|
return cache_key
|
|
|
|
|
|
# Global unified interface instance
|
|
unified_model_interface = UnifiedModelInterface()
|
|
|
|
|
|
def _resolve_torch_dtype(precision: str):
|
|
precision = (precision or "auto").lower()
|
|
dtype_map = {
|
|
"bf16": torch.bfloat16,
|
|
"bfloat16": torch.bfloat16,
|
|
"fp16": torch.float16,
|
|
"float16": torch.float16,
|
|
"fp32": torch.float32,
|
|
"float32": torch.float32,
|
|
}
|
|
if precision in dtype_map:
|
|
return dtype_map[precision]
|
|
|
|
if torch.cuda.is_available():
|
|
major, _minor = torch.cuda.get_device_capability()
|
|
return torch.bfloat16 if major >= 8 else torch.float16
|
|
return torch.float32
|
|
|
|
|
|
def _sanitize_loader_settings(device: str, torch_dtype, attn_implementation: str):
|
|
resolved_attn = "sdpa" if attn_implementation in ("auto", None, "") else attn_implementation
|
|
|
|
if str(device) == "cpu":
|
|
if torch_dtype == torch.float16:
|
|
torch_dtype = torch.float32
|
|
if resolved_attn == "flash_attention_2":
|
|
resolved_attn = "sdpa"
|
|
|
|
return torch_dtype, resolved_attn
|
|
|
|
|
|
# Convenience functions for common model operations
|
|
def _pop_runtime_kwargs(kwargs: Dict[str, Any]) -> Dict[str, Optional[str]]:
|
|
return {
|
|
"runtime_mode": kwargs.pop("runtime_mode", "main_environment"),
|
|
"runtime_profile": kwargs.pop("runtime_profile", None),
|
|
}
|
|
|
|
|
|
def load_tts_model(engine_name: str,
|
|
model_name: str,
|
|
device: str,
|
|
language: str = "English",
|
|
model_path: Optional[str] = None,
|
|
repo_id: Optional[str] = None,
|
|
force_reload: bool = False,
|
|
**kwargs) -> Any:
|
|
"""
|
|
Convenience function to load TTS models.
|
|
|
|
Args:
|
|
engine_name: Engine name ("chatterbox", "f5tts", etc.)
|
|
model_name: Model identifier
|
|
device: Target device
|
|
language: Model language
|
|
model_path: Local path if available
|
|
repo_id: HuggingFace repo ID if available
|
|
force_reload: Force reload even if cached
|
|
**kwargs: Additional engine-specific parameters
|
|
|
|
Returns:
|
|
Loaded TTS model
|
|
"""
|
|
runtime_kwargs = _pop_runtime_kwargs(kwargs)
|
|
config = ModelLoadConfig(
|
|
engine_name=engine_name,
|
|
model_type="tts",
|
|
model_name=model_name,
|
|
device=device,
|
|
language=language,
|
|
model_path=model_path,
|
|
repo_id=repo_id,
|
|
additional_params=kwargs,
|
|
runtime_mode=runtime_kwargs["runtime_mode"],
|
|
runtime_profile=runtime_kwargs["runtime_profile"],
|
|
)
|
|
return unified_model_interface.load_model(config, force_reload)
|
|
|
|
|
|
def load_vc_model(engine_name: str,
|
|
model_name: str,
|
|
device: str,
|
|
language: str = "English",
|
|
model_path: Optional[str] = None,
|
|
repo_id: Optional[str] = None,
|
|
force_reload: bool = False,
|
|
**kwargs) -> Any:
|
|
"""
|
|
Convenience function to load Voice Conversion models.
|
|
"""
|
|
runtime_kwargs = _pop_runtime_kwargs(kwargs)
|
|
config = ModelLoadConfig(
|
|
engine_name=engine_name,
|
|
model_type="vc",
|
|
model_name=model_name,
|
|
device=device,
|
|
language=language,
|
|
model_path=model_path,
|
|
repo_id=repo_id,
|
|
additional_params=kwargs,
|
|
runtime_mode=runtime_kwargs["runtime_mode"],
|
|
runtime_profile=runtime_kwargs["runtime_profile"],
|
|
)
|
|
return unified_model_interface.load_model(config, force_reload)
|
|
|
|
|
|
def load_auxiliary_model(engine_name: str,
|
|
model_type: str, # "tokenizer", "hubert", "separator", etc.
|
|
model_name: str,
|
|
device: str,
|
|
model_path: Optional[str] = None,
|
|
repo_id: Optional[str] = None,
|
|
force_reload: bool = False,
|
|
**kwargs) -> Any:
|
|
"""
|
|
Convenience function to load auxiliary models (tokenizers, HuBERT, etc.).
|
|
"""
|
|
runtime_kwargs = _pop_runtime_kwargs(kwargs)
|
|
config = ModelLoadConfig(
|
|
engine_name=engine_name,
|
|
model_type=model_type,
|
|
model_name=model_name,
|
|
device=device,
|
|
model_path=model_path,
|
|
repo_id=repo_id,
|
|
additional_params=kwargs,
|
|
runtime_mode=runtime_kwargs["runtime_mode"],
|
|
runtime_profile=runtime_kwargs["runtime_profile"],
|
|
)
|
|
return unified_model_interface.load_model(config, force_reload)
|
|
|
|
|
|
# Factory registration helpers
|
|
def register_chatterbox_factory():
|
|
"""Register ChatterBox model factories with universal component mixing logic"""
|
|
|
|
def load_chatterbox_with_mixing(target_class, device, language, model_path):
|
|
"""Universal ChatterBox loading with component mixing fallback"""
|
|
from engines.chatterbox.language_models import find_local_model_path
|
|
|
|
# Ensure target_class is available before attempting to use it
|
|
if target_class is None:
|
|
raise RuntimeError(f"ChatterBox class not available - cannot load model")
|
|
|
|
# Try provided path first
|
|
if model_path:
|
|
try:
|
|
return target_class.from_local(model_path, device, language)
|
|
except Exception as e:
|
|
print(f"⚠️ Failed to load from provided path {model_path}: {e}")
|
|
# Continue to fallback logic
|
|
|
|
# Try language-specific local model
|
|
try:
|
|
local_path = find_local_model_path(language)
|
|
if local_path:
|
|
return target_class.from_local(local_path, device, language)
|
|
except Exception as e:
|
|
print(f"⚠️ Failed to load local {language} model: {e}")
|
|
|
|
# Try HuggingFace download for requested language
|
|
try:
|
|
return target_class.from_pretrained(device, language)
|
|
except Exception as e:
|
|
error_str = str(e)
|
|
print(f"⚠️ Failed to download {language} model: {e}")
|
|
|
|
# Check for 401 authorization errors (gated/private repos)
|
|
if "401" in error_str or "Unauthorized" in error_str:
|
|
print(f"🔒 {language} model requires manual download due to authentication requirements")
|
|
print(f" Please visit the model repository and download manually to use this model")
|
|
|
|
# For German variants, fall back to base German model instead of English
|
|
if language.startswith("German (") and language != "German":
|
|
print(f"🔄 Falling back to base German model for ChatterBox")
|
|
# Try the complete fallback sequence for German
|
|
try:
|
|
german_local = find_local_model_path("German")
|
|
if german_local:
|
|
return target_class.from_local(german_local, device, "German")
|
|
except Exception as local_error:
|
|
print(f"⚠️ German local model failed: {local_error}")
|
|
|
|
# If local German fails, try downloading German
|
|
try:
|
|
print(f"📥 Attempting to download base German model")
|
|
return target_class.from_pretrained(device, "German")
|
|
except Exception as german_error:
|
|
print(f"⚠️ German download also failed: {german_error}")
|
|
# Continue to English fallback
|
|
|
|
# Final fallback: English model if not already trying English
|
|
if language != "English":
|
|
print(f"🔄 Falling back to English model for ChatterBox")
|
|
try:
|
|
# Try English local first
|
|
english_local = find_local_model_path("English")
|
|
if english_local:
|
|
return target_class.from_local(english_local, device, "English")
|
|
else:
|
|
return target_class.from_pretrained(device, "English")
|
|
except Exception as english_error:
|
|
print(f"❌ English fallback also failed: {english_error}")
|
|
raise e # Raise original error
|
|
else:
|
|
raise e # Already trying English, fail
|
|
|
|
def chatterbox_tts_factory(config: ModelLoadConfig):
|
|
try:
|
|
from engines.chatterbox.tts import ChatterboxTTS
|
|
except ImportError:
|
|
ChatterboxTTS = None
|
|
|
|
if ChatterboxTTS is None:
|
|
raise RuntimeError("ChatterboxTTS not available - check installation")
|
|
|
|
return load_chatterbox_with_mixing(ChatterboxTTS, config.device, config.language or "English", config.model_path)
|
|
|
|
def chatterbox_vc_factory(config: ModelLoadConfig):
|
|
try:
|
|
from engines.chatterbox.vc import ChatterboxVC
|
|
from engines.chatterbox.language_models import supports_voice_conversion, get_vc_supported_languages
|
|
except ImportError:
|
|
ChatterboxVC = None
|
|
supports_voice_conversion = None
|
|
|
|
if ChatterboxVC is None:
|
|
raise RuntimeError("ChatterboxVC not available - check installation or add bundled version")
|
|
|
|
language = config.language or "English"
|
|
|
|
# Check if language supports VC before attempting to load
|
|
if supports_voice_conversion and not supports_voice_conversion(language):
|
|
print(f"❌ {language} model does not support voice conversion")
|
|
print(f" Voice conversion requires s3gen component which is missing from this model")
|
|
print(f" Try English model (confirmed working) or German models (tested working)")
|
|
print(f" Other language models may not have VC components")
|
|
|
|
raise RuntimeError(f"Voice conversion not supported for {language} model. "
|
|
f"Try English or German models instead.")
|
|
|
|
return load_chatterbox_with_mixing(ChatterboxVC, config.device, language, config.model_path)
|
|
|
|
unified_model_interface.register_model_factory("chatterbox", "tts", chatterbox_tts_factory)
|
|
unified_model_interface.register_model_factory("chatterbox", "vc", chatterbox_vc_factory)
|
|
|
|
|
|
def register_f5tts_factory():
|
|
"""Register F5-TTS model factories"""
|
|
def f5tts_factory(config: ModelLoadConfig):
|
|
from engines.f5tts.f5tts import ChatterBoxF5TTS
|
|
import os
|
|
import folder_paths
|
|
|
|
model_name = config.model_name or "F5TTS_Base"
|
|
|
|
# Try local first, then HuggingFace
|
|
try:
|
|
# Check for local models
|
|
search_paths = [
|
|
os.path.join(folder_paths.models_dir, "TTS", "F5-TTS", model_name),
|
|
os.path.join(folder_paths.models_dir, "F5-TTS", model_name), # Legacy
|
|
os.path.join(folder_paths.models_dir, "Checkpoints", "F5-TTS", model_name) # Legacy
|
|
]
|
|
|
|
local_path = None
|
|
for path in search_paths:
|
|
if os.path.exists(path):
|
|
local_path = path
|
|
break
|
|
|
|
if local_path:
|
|
print(f"📁 Using local F5-TTS model: {local_path}")
|
|
return ChatterBoxF5TTS.from_local(local_path, config.device, model_name)
|
|
else:
|
|
print(f"📥 Loading F5-TTS model '{model_name}' from HuggingFace")
|
|
return ChatterBoxF5TTS.from_pretrained(config.device, model_name)
|
|
|
|
except Exception as e:
|
|
raise RuntimeError(f"Failed to load F5-TTS model '{model_name}': {e}")
|
|
|
|
unified_model_interface.register_model_factory("f5tts", "tts", f5tts_factory)
|
|
|
|
|
|
def register_higgs_audio_factory():
|
|
"""Register Higgs Audio model factories with stateless wrapper for ComfyUI integration"""
|
|
def higgs_audio_factory(config: ModelLoadConfig):
|
|
from engines.higgs_audio.boson_multimodal.serve.serve_engine import HiggsAudioServeEngine
|
|
from engines.higgs_audio.higgs_audio import HiggsAudioEngine
|
|
from engines.higgs_audio.stateless_wrapper import create_stateless_higgs_wrapper
|
|
|
|
model_name = config.model_name
|
|
device = config.device or "cuda"
|
|
model_path = config.model_path or model_name
|
|
tokenizer_path = config.additional_params.get("tokenizer_path") if config.additional_params else None
|
|
enable_cuda_graphs = config.additional_params.get("enable_cuda_graphs", True) if config.additional_params else True
|
|
|
|
# Check if we need to force recreate due to CUDA Graph corruption
|
|
force_recreate = config.additional_params.get("_force_recreate", False) if config.additional_params else False
|
|
if force_recreate:
|
|
print(f"🔥 Force recreating Higgs Audio engine due to CUDA Graph corruption")
|
|
|
|
# Log CUDA Graph setting
|
|
perf_mode = "High Performance (CUDA Graphs ON)" if enable_cuda_graphs else "Memory Safe (CUDA Graphs OFF)"
|
|
print(f"🔧 Higgs Audio mode: {perf_mode}")
|
|
|
|
# Create the base HiggsAudioEngine but initialize manually to avoid circular dependency
|
|
base_engine = HiggsAudioEngine()
|
|
|
|
# Manually set up the engine properties to avoid calling initialize_engine
|
|
base_engine.model_path = model_path
|
|
base_engine.tokenizer_path = tokenizer_path
|
|
base_engine.device = device
|
|
|
|
# Get smart paths using the engine's methods
|
|
model_path_for_engine = base_engine._get_smart_model_path(model_path)
|
|
tokenizer_path_for_engine = base_engine._get_smart_tokenizer_path(tokenizer_path)
|
|
|
|
# Configure CUDA Graph settings based on toggle
|
|
if enable_cuda_graphs:
|
|
print(f"🚀 Loading Higgs Audio model directly on {device} for optimal performance...")
|
|
cache_type = "StaticCache"
|
|
kv_cache_lengths = [1024, 2048, 4096] # StaticCache for CUDA Graph optimization
|
|
else:
|
|
print(f"🚀 Loading Higgs Audio model directly on {device} (CUDA Graphs disabled for memory safety)...")
|
|
cache_type = "DynamicCache"
|
|
# Still need cache buckets but they won't use StaticCache
|
|
kv_cache_lengths = [1024, 2048, 4096] # Same sizes but will use DynamicCache
|
|
|
|
# Disable CUDA Graph compilation at environment level
|
|
import os
|
|
os.environ['PYTORCH_DISABLE_CUDA_GRAPH'] = '1'
|
|
os.environ['TORCH_COMPILE_DISABLE'] = '1'
|
|
|
|
serve_engine = HiggsAudioServeEngine(
|
|
model_name_or_path=model_path_for_engine,
|
|
audio_tokenizer_name_or_path=tokenizer_path_for_engine,
|
|
device=device,
|
|
kv_cache_lengths=kv_cache_lengths,
|
|
enable_cuda_graphs=enable_cuda_graphs
|
|
)
|
|
|
|
# Settings are already stored by the constructor
|
|
|
|
print(f"✅ Higgs Audio model loaded directly on {device}")
|
|
|
|
# Set the serve engine on the base engine
|
|
base_engine.engine = serve_engine
|
|
|
|
# Wrap with stateless wrapper for ComfyUI model management compatibility
|
|
stateless_wrapper = create_stateless_higgs_wrapper(base_engine)
|
|
|
|
print(f"📦 Higgs Audio engine ready (via unified interface)")
|
|
return stateless_wrapper
|
|
|
|
unified_model_interface.register_model_factory("higgs_audio", "tts", higgs_audio_factory)
|
|
|
|
|
|
def register_higgs_audio_v3_factory():
|
|
"""Register native Higgs Audio v3 model factory."""
|
|
|
|
def higgs_audio_v3_factory(config: ModelLoadConfig):
|
|
from engines.higgs_audio_v3.higgs_audio_v3 import HiggsAudioV3Engine
|
|
from engines.higgs_audio_v3.higgs_audio_v3_downloader import HiggsAudioV3Downloader
|
|
|
|
model_name = config.model_name or "higgs-audio-v3-tts-4b"
|
|
model_path = config.model_path or model_name
|
|
device = config.device or "auto"
|
|
|
|
additional = config.additional_params or {}
|
|
dtype = additional.get("dtype", "auto")
|
|
attention = additional.get("attention", "auto")
|
|
|
|
downloader = HiggsAudioV3Downloader()
|
|
resolved_model_path = downloader.resolve_model_path(model_path)
|
|
|
|
print("🔄 Loading Higgs Audio v3 via unified interface")
|
|
print(f" Model: {model_name}")
|
|
print(f" Path: {resolved_model_path}")
|
|
print(f" Device: {device} | Dtype: {dtype} | Attention: {attention}")
|
|
|
|
return HiggsAudioV3Engine(
|
|
model_path=resolved_model_path,
|
|
device=device,
|
|
dtype=dtype,
|
|
attention=attention,
|
|
)
|
|
|
|
unified_model_interface.register_model_factory("higgs_audio_v3", "tts", higgs_audio_v3_factory)
|
|
|
|
|
|
def register_rvc_factory():
|
|
"""Register RVC model factories"""
|
|
def rvc_factory(config: ModelLoadConfig):
|
|
"""Load RVC model and wrap it for ComfyUI management"""
|
|
from engines.rvc.impl.vc_infer_pipeline import get_vc
|
|
from engines.rvc.impl.config import config as rvc_config
|
|
from engines.rvc.rvc_engine import RVCModelWrapper
|
|
|
|
model_path = config.model_path
|
|
index_path = config.additional_params.get("index_path") if config.additional_params else None
|
|
device = config.device or "cuda"
|
|
|
|
if not model_path:
|
|
raise ValueError("RVC factory requires model_path")
|
|
|
|
# Load the RVC model using the actual implementation
|
|
model_data = get_vc(model_path, index_path, rvc_config, device)
|
|
|
|
# Wrap the model_data dict so it has .to() method for device management
|
|
wrapped_model = RVCModelWrapper(model_data, device)
|
|
|
|
return wrapped_model
|
|
|
|
def hubert_factory(config: ModelLoadConfig):
|
|
from engines.rvc.impl.lib.model_utils import load_hubert
|
|
model_path = config.model_path
|
|
config_dict = config.additional_params.get("config", {}) if config.additional_params else {}
|
|
return load_hubert(model_path, config_dict)
|
|
|
|
def mdx23c_separator_factory(config: ModelLoadConfig):
|
|
from engines.rvc.impl.lib.mdx23c import MDX23C
|
|
model_path = config.model_path
|
|
device = config.device or "cuda"
|
|
return MDX23C(model_path, device)
|
|
|
|
def demucs_separator_factory(config: ModelLoadConfig):
|
|
from engines.rvc.impl.lib.uvr5_pack.demucs.pretrained import get_model
|
|
model_name = config.model_name or "htdemucs"
|
|
device = config.device or "cuda"
|
|
return get_model(model_name).to(device)
|
|
|
|
unified_model_interface.register_model_factory("rvc", "vc", rvc_factory)
|
|
unified_model_interface.register_model_factory("rvc", "hubert", hubert_factory)
|
|
unified_model_interface.register_model_factory("rvc", "separator_mdx", mdx23c_separator_factory)
|
|
unified_model_interface.register_model_factory("rvc", "separator_demucs", demucs_separator_factory)
|
|
|
|
|
|
def register_vibevoice_factory():
|
|
"""Register VibeVoice model factory"""
|
|
def vibevoice_factory(config: ModelLoadConfig):
|
|
"""Factory for VibeVoice models with ComfyUI integration"""
|
|
|
|
# Ensure accelerate is imported before VibeVoice engine
|
|
try:
|
|
import accelerate
|
|
# (accelerate version check removed - only show on errors)
|
|
except ImportError as e:
|
|
print(f"⚠️ Unified Interface: accelerate not available: {e}")
|
|
|
|
from engines.vibevoice_engine import VibeVoiceEngine
|
|
|
|
# Extract parameters
|
|
model_name = config.model_name or "vibevoice-1.5B"
|
|
device = config.device or "auto"
|
|
attention_mode = config.additional_params.get("attention_mode", "auto") if config.additional_params else "auto"
|
|
quantize_llm_4bit = config.additional_params.get("quantize_llm_4bit", False) if config.additional_params else False
|
|
|
|
# Create engine instance
|
|
engine = VibeVoiceEngine()
|
|
|
|
# Initialize with parameters
|
|
engine.initialize_engine(
|
|
model_name=model_name,
|
|
device=device,
|
|
attention_mode=attention_mode,
|
|
quantize_llm_4bit=quantize_llm_4bit
|
|
)
|
|
|
|
print(f"✅ VibeVoice model '{model_name}' loaded via unified interface")
|
|
|
|
# Return the engine which contains both model and processor
|
|
return engine
|
|
|
|
unified_model_interface.register_model_factory("vibevoice", "tts", vibevoice_factory)
|
|
|
|
|
|
def register_index_tts_factory():
|
|
"""Register the shared IndexTTS-2 / IndexTTS-2.5 model factory."""
|
|
def index_tts_factory(config: ModelLoadConfig):
|
|
"""Factory for versioned IndexTTS models with ComfyUI integration."""
|
|
import os
|
|
import sys
|
|
|
|
# Extract parameters
|
|
model_path = config.model_path
|
|
device = config.device or "auto"
|
|
use_fp16 = config.additional_params.get("use_fp16", True) if config.additional_params else True
|
|
use_cuda_kernel = config.additional_params.get("use_cuda_kernel", None) if config.additional_params else None
|
|
use_deepspeed = config.additional_params.get("use_deepspeed", False) if config.additional_params else False
|
|
use_torch_compile = config.additional_params.get("use_torch_compile", False) if config.additional_params else False
|
|
use_accel = config.additional_params.get("use_accel", False) if config.additional_params else False
|
|
low_vram = config.additional_params.get("low_vram", False) if config.additional_params else False
|
|
requested_version = config.additional_params.get("model_version") if config.additional_params else None
|
|
is_v25 = requested_version == "2.5" or (
|
|
requested_version is None and os.path.isfile(os.path.join(model_path or "", "codec.pth"))
|
|
)
|
|
version_label = "IndexTTS-2.5" if is_v25 else "IndexTTS-2"
|
|
|
|
if not model_path or not os.path.exists(model_path):
|
|
raise RuntimeError(f"{version_label} model not found at {model_path}. Auto-download should have been triggered earlier.")
|
|
|
|
try:
|
|
# Add bundled IndexTTS path to sys.path so internal imports work
|
|
import sys
|
|
bundled_path = os.path.join(os.path.dirname(__file__), "..", "..", "engines", "index_tts")
|
|
bundled_path = os.path.abspath(bundled_path)
|
|
if bundled_path not in sys.path:
|
|
sys.path.insert(0, bundled_path)
|
|
|
|
# Check for conflicting external IndexTTS installation
|
|
try:
|
|
import indextts
|
|
external_path = os.path.abspath(indextts.__file__)
|
|
|
|
# Check if the imported indextts is from our bundled version
|
|
# by verifying it's inside our engines/index_tts directory
|
|
expected_bundled_path = os.path.abspath(
|
|
os.path.join(os.path.dirname(__file__), "..", "..", "engines", "index_tts", "indextts")
|
|
)
|
|
|
|
# If indextts is NOT from our bundled path, it's an external installation
|
|
if not external_path.startswith(expected_bundled_path):
|
|
raise ImportError(f"""
|
|
❌ External IndexTTS installation detected!
|
|
|
|
Found at: {external_path}
|
|
Expected bundled path: {expected_bundled_path}
|
|
|
|
This conflicts with our bundled IndexTTS-2 engine and causes import errors.
|
|
|
|
🔧 SOLUTION: Please uninstall the external version:
|
|
pip uninstall indextts
|
|
|
|
Then restart ComfyUI.
|
|
|
|
Our bundled version has all required dependencies and will work perfectly.
|
|
""")
|
|
except ImportError as e:
|
|
if "External IndexTTS" in str(e):
|
|
raise e
|
|
# No external installation found - this is good!
|
|
pass
|
|
except Exception:
|
|
# Some other issue with indextts, proceed anyway
|
|
pass
|
|
|
|
# TTS Audio Suite patch: Select the backend by model version while
|
|
# preserving the stable index_tts engine/cache identity.
|
|
if is_v25:
|
|
from engines.index_tts.indextts.infer_v2_5 import IndexTTS2
|
|
else:
|
|
from engines.index_tts.indextts.infer_v2 import IndexTTS2
|
|
|
|
# Initialize IndexTTS-2 engine
|
|
config_path = os.path.join(model_path, "config.yaml")
|
|
|
|
# Verify config file exists after download
|
|
if not os.path.exists(config_path):
|
|
raise RuntimeError(f"{version_label} config.yaml not found at {config_path} even after download. Please check model integrity.")
|
|
|
|
common_kwargs = dict(
|
|
cfg_path=config_path,
|
|
model_dir=model_path,
|
|
device=device,
|
|
use_cuda_kernel=use_cuda_kernel,
|
|
use_deepspeed=use_deepspeed,
|
|
use_torch_compile=use_torch_compile,
|
|
use_accel=use_accel,
|
|
low_vram=low_vram,
|
|
)
|
|
if is_v25:
|
|
qwen_dir = os.path.join(model_path, "qwen0.6bemo4-merge")
|
|
engine = IndexTTS2(
|
|
use_bf16=use_fp16 and device != "cpu",
|
|
use_qwen_emo=os.path.isdir(qwen_dir),
|
|
**common_kwargs,
|
|
)
|
|
else:
|
|
engine = IndexTTS2(
|
|
use_fp16=use_fp16 and device != "cpu",
|
|
**common_kwargs,
|
|
)
|
|
|
|
print(f"✅ {version_label} model loaded via unified interface on {device}")
|
|
return engine
|
|
|
|
except ImportError as e:
|
|
raise ImportError(f"{version_label} dependencies not available. Error: {e}")
|
|
except Exception as e:
|
|
raise RuntimeError(f"Failed to load {version_label} model: {e}")
|
|
|
|
unified_model_interface.register_model_factory("index_tts", "tts", index_tts_factory)
|
|
|
|
def register_chatterbox_23lang_factory():
|
|
"""Register ChatterBox Official 23-Lang model factory"""
|
|
def chatterbox_23lang_factory(config: ModelLoadConfig):
|
|
"""Factory for ChatterBox Official 23-Lang models with ComfyUI integration"""
|
|
from engines.chatterbox_official_23lang.tts import ChatterboxOfficial23LangTTS
|
|
import folder_paths
|
|
import os
|
|
import torch
|
|
|
|
# Extract parameters
|
|
device = config.device or "auto"
|
|
model_name = config.model_name or "ChatterBox Official 23-Lang"
|
|
language = config.language or "english" # This is the actual language to use
|
|
model_version = config.additional_params.get("model_version", "v2") if config.additional_params else "v2" # v1 or v2
|
|
|
|
# Determine device
|
|
if device == "auto":
|
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
|
|
# Get model directory path - use model_name for the directory
|
|
# For "Vietnamese (Viterbox)", this will be "Vietnamese (Viterbox)"
|
|
# For "Egyptian Arabic (oddadmix)", this will be "Egyptian Arabic (oddadmix)"
|
|
# For "ChatterBox Official 23-Lang", this will be "ChatterBox Official 23-Lang"
|
|
models_dir = folder_paths.models_dir
|
|
# Strip "ChatterBox " prefix for directory structure consistency
|
|
dir_name = model_name.replace("ChatterBox ", "") if model_name.startswith("ChatterBox ") else model_name
|
|
ckpt_dir = os.path.join(models_dir, "TTS", "chatterbox_official_23lang", dir_name)
|
|
|
|
print(f"🌍 Loading {model_name} model for {language} on {device}")
|
|
print(f"📁 Using model directory: {ckpt_dir}")
|
|
|
|
# Try local first, then use from_pretrained for auto-download if needed
|
|
if os.path.exists(ckpt_dir):
|
|
try:
|
|
engine = ChatterboxOfficial23LangTTS.from_local(
|
|
ckpt_dir=ckpt_dir,
|
|
device=device,
|
|
model_name=model_name,
|
|
model_version=model_version
|
|
)
|
|
print(f"✅ {model_name} '{language}' loaded via unified interface")
|
|
return engine
|
|
except FileNotFoundError as e:
|
|
print(f"⚠️ Local model incomplete: {e}")
|
|
print(f"📥 Downloading missing {model_version} files...")
|
|
|
|
# Use from_pretrained for auto-download
|
|
engine = ChatterboxOfficial23LangTTS.from_pretrained(
|
|
device=device,
|
|
model_name=model_name,
|
|
model_version=model_version
|
|
)
|
|
|
|
print(f"✅ ChatterBox Official 23-Lang '{language}' loaded via unified interface")
|
|
|
|
# Return the engine
|
|
return engine
|
|
|
|
unified_model_interface.register_model_factory("chatterbox_official_23lang", "tts", chatterbox_23lang_factory)
|
|
|
|
|
|
def register_step_audio_editx_factory():
|
|
"""Register Step Audio EditX model factory"""
|
|
def step_audio_editx_factory(config: ModelLoadConfig):
|
|
"""Factory for Step Audio EditX models with ComfyUI integration"""
|
|
import os
|
|
import sys
|
|
import torch
|
|
|
|
# Extract parameters
|
|
model_path = config.model_path
|
|
device = config.device or "auto"
|
|
torch_dtype_str = config.additional_params.get("torch_dtype", "bfloat16") if config.additional_params else "bfloat16"
|
|
quantization = config.additional_params.get("quantization", None) if config.additional_params else None
|
|
|
|
# Resolve model path using downloader (handles "local:" prefix and auto-download)
|
|
from engines.step_audio_editx.step_audio_editx_downloader import StepAudioEditXDownloader
|
|
downloader = StepAudioEditXDownloader()
|
|
model_path = downloader.resolve_model_path(model_path)
|
|
|
|
if not model_path or not os.path.exists(model_path):
|
|
raise RuntimeError(f"Step Audio EditX model not found at {model_path}. Auto-download should have been triggered earlier.")
|
|
|
|
try:
|
|
# Add bundled Step Audio EditX path to sys.path so internal imports work
|
|
bundled_path = os.path.join(os.path.dirname(__file__), "..", "..", "engines", "step_audio_editx")
|
|
bundled_path = os.path.abspath(bundled_path)
|
|
if bundled_path not in sys.path:
|
|
sys.path.insert(0, bundled_path)
|
|
|
|
# Import from bundled Step Audio EditX engine
|
|
from engines.step_audio_editx.step_audio_editx_impl.tts import StepAudioTTS
|
|
from engines.step_audio_editx.step_audio_editx_impl.tokenizer import StepAudioTokenizer
|
|
from engines.step_audio_editx.step_audio_editx_impl.model_loader import ModelSource
|
|
|
|
# Resolve torch dtype
|
|
dtype_map = {
|
|
"bfloat16": torch.bfloat16,
|
|
"float16": torch.float16,
|
|
"float32": torch.float32,
|
|
"auto": torch.bfloat16 if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else torch.float16
|
|
}
|
|
torch_dtype = dtype_map.get(torch_dtype_str, torch.bfloat16)
|
|
|
|
# Resolve device
|
|
if device == "auto":
|
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
|
|
# Resolve quantization config
|
|
quantization_config = None
|
|
if quantization and quantization != "none":
|
|
quantization_config = quantization # Will be 'int4', 'int8', etc.
|
|
|
|
# Auto-download all required models if not present
|
|
from engines.step_audio_editx.step_audio_editx_downloader import StepAudioEditXDownloader
|
|
downloader = StepAudioEditXDownloader()
|
|
base_path = os.path.dirname(model_path)
|
|
|
|
# Check and download Step-Audio-EditX main model (includes tokenizer files)
|
|
main_files = downloader.MODELS["Step-Audio-EditX"]["files"]
|
|
if main_files and isinstance(main_files[0], dict):
|
|
check_files = [f["local"] for f in main_files]
|
|
else:
|
|
check_files = main_files
|
|
|
|
# Also add additional downloads to completeness check
|
|
additional_downloads = downloader.MODELS["Step-Audio-EditX"].get("additional_downloads", [])
|
|
for additional in additional_downloads:
|
|
for file_dict in additional["files"]:
|
|
check_files.append(file_dict["local"])
|
|
|
|
is_complete = downloader._is_model_complete(model_path, check_files)
|
|
|
|
if not is_complete:
|
|
print(f"📥 Step-Audio-EditX model incomplete, downloading...")
|
|
downloader.download_model("Step-Audio-EditX")
|
|
|
|
# Check and download FunASR model (required for tokenizer)
|
|
funasr_path = os.path.join(base_path, "FunASR-Paraformer")
|
|
funasr_files = downloader.MODELS["FunASR-Paraformer"]["files"]
|
|
if not downloader._is_model_complete(funasr_path, funasr_files):
|
|
print(f"📥 FunASR model not found or incomplete, downloading (one-time setup)...")
|
|
downloader.download_model("FunASR-Paraformer")
|
|
|
|
# CRITICAL: Verify CosyVoice model exists (required for audio synthesis)
|
|
# CosyVoice is a subfolder of the main model and must be complete
|
|
cosy_voice_path = os.path.join(model_path, "CosyVoice-300M-25Hz")
|
|
cosy_voice_files = [
|
|
"cosyvoice.yaml", "flow.pt", "hift.pt",
|
|
"campplus.onnx", "speech_tokenizer_v1.onnx", "FLOW_VERSION"
|
|
]
|
|
|
|
if not os.path.exists(cosy_voice_path):
|
|
raise RuntimeError(
|
|
f"❌ CosyVoice model not found at: {cosy_voice_path}\n"
|
|
f"This subfolder should have been downloaded with Step-Audio-EditX.\n"
|
|
f"To fix: Delete the entire Step-Audio-EditX folder and re-run to trigger full download.\n"
|
|
f"Model path: {model_path}"
|
|
)
|
|
|
|
# Check CosyVoice completeness
|
|
missing_files = []
|
|
for file in cosy_voice_files:
|
|
file_path = os.path.join(cosy_voice_path, file)
|
|
if not os.path.exists(file_path):
|
|
missing_files.append(file)
|
|
|
|
if missing_files:
|
|
raise RuntimeError(
|
|
f"❌ CosyVoice model incomplete. Missing files: {', '.join(missing_files)}\n"
|
|
f"Location: {cosy_voice_path}\n"
|
|
f"To fix: Delete the CosyVoice-300M-25Hz folder and re-run to trigger re-download."
|
|
)
|
|
|
|
# Note: cosyvoice.yaml uses HyperPyYAML custom tags (!new:, !ref, etc.)
|
|
# so we cannot validate it with standard yaml.safe_load().
|
|
# The actual validation happens when CosyVoice loads it with load_hyperpyyaml().
|
|
|
|
# Initialize tokenizer (silent - errors will be raised if fails)
|
|
tokenizer = StepAudioTokenizer(
|
|
encoder_path=model_path,
|
|
model_source=ModelSource.LOCAL # Always use local for now
|
|
)
|
|
|
|
# Initialize TTS engine (silent - errors will be raised if fails)
|
|
tts_engine = StepAudioTTS(
|
|
model_path=model_path,
|
|
audio_tokenizer=tokenizer,
|
|
model_source=ModelSource.LOCAL,
|
|
quantization_config=quantization_config,
|
|
torch_dtype=torch_dtype,
|
|
device_map=device
|
|
)
|
|
|
|
# Return wrapped engine directly instead of using wrapper class
|
|
# The adapter and nodes expect a wrapper with clone() and edit_single() methods
|
|
# Create a simple wrapper that delegates to the raw TTS engine
|
|
class StepAudioEditXEngineWrapper:
|
|
"""Simple wrapper providing clone() and edit_single() methods for the raw StepAudioTTS engine"""
|
|
def __init__(self, tts_engine, tokenizer, device, torch_dtype, quantization, model_path):
|
|
self._tts_engine = tts_engine
|
|
self._tokenizer = tokenizer
|
|
self._model_config = None
|
|
self.device = device
|
|
self.torch_dtype = torch_dtype
|
|
self.quantization = quantization
|
|
self.model_dir = model_path
|
|
|
|
def clone(self, prompt_wav_path, prompt_text, target_text, temperature=0.7, do_sample=True, max_new_tokens=1024, progress_bar=None):
|
|
"""Delegate clone to raw TTS engine"""
|
|
return self._tts_engine.clone(
|
|
prompt_wav_path=prompt_wav_path,
|
|
prompt_text=prompt_text,
|
|
target_text=target_text,
|
|
progress_bar=progress_bar,
|
|
max_new_tokens=max_new_tokens,
|
|
temperature=temperature,
|
|
do_sample=do_sample
|
|
)
|
|
|
|
def edit_single(self, input_audio_path, audio_text, edit_type, edit_info=None, text=None, progress_bar=None, max_new_tokens=1024, temperature=0.7, do_sample=True):
|
|
"""Delegate edit_single to raw TTS engine"""
|
|
audio_tensor, sample_rate = self._tts_engine.edit(
|
|
input_audio_path=input_audio_path,
|
|
audio_text=audio_text,
|
|
edit_type=edit_type,
|
|
edit_info=edit_info,
|
|
text=text,
|
|
progress_bar=progress_bar,
|
|
max_new_tokens=max_new_tokens,
|
|
temperature=temperature,
|
|
do_sample=do_sample
|
|
)
|
|
# Ensure output is [1, samples] format (2D)
|
|
if audio_tensor.dim() == 1:
|
|
audio_tensor = audio_tensor.unsqueeze(0)
|
|
elif audio_tensor.dim() == 3:
|
|
audio_tensor = audio_tensor.squeeze(0)
|
|
return audio_tensor
|
|
|
|
def edit(self, input_audio_path, audio_text, edit_type, edit_info=None, text=None, progress_bar=None, max_new_tokens=1024, temperature=0.7, do_sample=True):
|
|
"""Alias for edit_single for compatibility"""
|
|
return self.edit_single(input_audio_path, audio_text, edit_type, edit_info, text, progress_bar, max_new_tokens, temperature, do_sample)
|
|
|
|
def get_sample_rate(self):
|
|
"""Get sample rate from raw engine"""
|
|
return 24000 # CosyVoice native sample rate
|
|
|
|
def to(self, device):
|
|
"""Move model to device"""
|
|
self.device = device
|
|
if hasattr(self._tts_engine, 'llm') and hasattr(self._tts_engine.llm, 'to'):
|
|
try:
|
|
self._tts_engine.llm.to(device)
|
|
except:
|
|
pass # Quantized models can't be moved
|
|
if hasattr(self._tts_engine, 'cosy_model') and hasattr(self._tts_engine.cosy_model, 'to'):
|
|
try:
|
|
self._tts_engine.cosy_model.to(device)
|
|
except:
|
|
pass
|
|
|
|
wrapper = StepAudioEditXEngineWrapper(tts_engine, tokenizer, device, torch_dtype, quantization, model_path)
|
|
print(f"✅ Step Audio EditX model loaded via unified interface on {device}")
|
|
return wrapper
|
|
|
|
except ImportError as e:
|
|
raise ImportError(f"Step Audio EditX dependencies not available. Error: {e}")
|
|
except Exception as e:
|
|
raise RuntimeError(f"Failed to load Step Audio EditX model: {e}")
|
|
|
|
unified_model_interface.register_model_factory("step_audio_editx", "tts", step_audio_editx_factory)
|
|
|
|
|
|
def register_echo_tts_factory():
|
|
"""Register Echo-TTS model factory"""
|
|
def echo_tts_factory(config: ModelLoadConfig):
|
|
"""Factory for Echo-TTS models with ComfyUI integration"""
|
|
import os
|
|
import torch
|
|
from huggingface_hub import hf_hub_download
|
|
from utils.models.extra_paths import get_preferred_download_path
|
|
from utils.device import resolve_torch_device
|
|
|
|
from echo_tts.inference import (
|
|
load_model_from_hf,
|
|
load_fish_ae_from_hf,
|
|
load_pca_state_from_hf,
|
|
sample_pipeline,
|
|
)
|
|
|
|
model_id = config.model_name or "jordand/echo-tts-base"
|
|
device = resolve_torch_device(config.device or "auto")
|
|
|
|
# Ensure HF downloads land under ComfyUI/models/TTS/<model name>
|
|
base_dir = get_preferred_download_path("TTS")
|
|
os.makedirs(base_dir, exist_ok=True)
|
|
model_name = model_id.split("/")[-1]
|
|
|
|
def _hf_download(repo_id: str, filename: str) -> None:
|
|
repo_dir = os.path.join(base_dir, repo_id.split("/")[-1])
|
|
os.makedirs(repo_dir, exist_ok=True)
|
|
hf_hub_download(
|
|
repo_id=repo_id,
|
|
filename=filename,
|
|
local_dir=repo_dir,
|
|
local_dir_use_symlinks=False,
|
|
)
|
|
|
|
# Download required files into ComfyUI models dir
|
|
_hf_download(model_id, "pytorch_model.safetensors")
|
|
_hf_download(model_id, "pca_state.safetensors")
|
|
_hf_download("jordand/fish-s1-dac-min", "pytorch_model.safetensors")
|
|
|
|
model_dtype = torch.bfloat16 if device == "cuda" else torch.float32
|
|
|
|
model = load_model_from_hf(
|
|
repo_id=model_name,
|
|
device=device,
|
|
dtype=model_dtype,
|
|
model_path=base_dir,
|
|
)
|
|
|
|
ae = load_fish_ae_from_hf(
|
|
repo_id="fish-s1-dac-min",
|
|
device=device,
|
|
dtype=model_dtype,
|
|
model_path=base_dir,
|
|
)
|
|
|
|
pca_state = load_pca_state_from_hf(
|
|
repo_id=model_name,
|
|
device=device,
|
|
model_path=base_dir,
|
|
)
|
|
|
|
class EchoTTSModelBundle:
|
|
def __init__(self, model, ae, pca_state, sample_pipeline, device):
|
|
self.model = model
|
|
self.ae = ae
|
|
self.pca_state = pca_state
|
|
self.sample_pipeline = sample_pipeline
|
|
self.device = device
|
|
|
|
def to(self, target_device):
|
|
if hasattr(self.model, "to"):
|
|
self.model = self.model.to(target_device)
|
|
if hasattr(self.ae, "to"):
|
|
self.ae = self.ae.to(target_device)
|
|
# pca_state is a dict of tensors loaded under inference_mode.
|
|
# Clone before moving to escape inference-tensor version tracking errors
|
|
# after ComfyUI "Unload Models" / VRAM clear cycles.
|
|
if isinstance(self.pca_state, dict):
|
|
self.pca_state = {
|
|
k: v.clone().to(target_device) if isinstance(v, torch.Tensor) else v
|
|
for k, v in self.pca_state.items()
|
|
}
|
|
elif hasattr(self.pca_state, "to"):
|
|
self.pca_state = self.pca_state.to(target_device)
|
|
self.device = target_device
|
|
return self
|
|
|
|
print(f"✅ Echo-TTS model '{model_name}' loaded via unified interface")
|
|
return EchoTTSModelBundle(model, ae, pca_state, sample_pipeline, device)
|
|
|
|
unified_model_interface.register_model_factory("echo_tts", "tts", echo_tts_factory)
|
|
|
|
|
|
def register_cosyvoice_factory():
|
|
"""Register CosyVoice3 model factory"""
|
|
def cosyvoice_factory(config: ModelLoadConfig):
|
|
"""Factory for CosyVoice3 models with ComfyUI integration"""
|
|
import os
|
|
import sys
|
|
|
|
# Extract parameters
|
|
model_path = config.model_path
|
|
device = config.device or "auto"
|
|
use_fp16 = config.additional_params.get("use_fp16", True) if config.additional_params else True
|
|
load_trt = config.additional_params.get("load_trt", False) if config.additional_params else False
|
|
load_vllm = config.additional_params.get("load_vllm", False) if config.additional_params else False
|
|
llm_filename = config.additional_params.get("llm_filename", "llm.pt") if config.additional_params else "llm.pt"
|
|
|
|
try:
|
|
# Import CosyVoice from bundled location
|
|
# First, add the bundled CosyVoice path to sys.path
|
|
current_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) # TTS-Audio-Suite root
|
|
bundled_cosyvoice_path = os.path.join(current_dir, "engines", "cosyvoice", "impl")
|
|
|
|
if os.path.exists(bundled_cosyvoice_path):
|
|
# Add bundled path to sys.path if not already there
|
|
if bundled_cosyvoice_path not in sys.path:
|
|
sys.path.insert(0, bundled_cosyvoice_path)
|
|
# print(f"📦 Using bundled CosyVoice library from {bundled_cosyvoice_path}") # Internal path, not useful for users
|
|
|
|
# Also add third_party/Matcha-TTS to path (required by CosyVoice)
|
|
matcha_path = os.path.join(bundled_cosyvoice_path, "third_party", "Matcha-TTS")
|
|
if os.path.exists(matcha_path) and matcha_path not in sys.path:
|
|
sys.path.insert(0, matcha_path)
|
|
|
|
try:
|
|
from cosyvoice.cli.cosyvoice import CosyVoice3
|
|
except ImportError as bundle_error:
|
|
raise ImportError(
|
|
f"Failed to import bundled CosyVoice: {bundle_error}\n"
|
|
"The bundled library may have missing dependencies.\n"
|
|
"Try: pip install hyperpyyaml conformer onnxruntime"
|
|
)
|
|
else:
|
|
# Fallback to system installation
|
|
try:
|
|
from cosyvoice.cli.cosyvoice import CosyVoice3
|
|
except ImportError:
|
|
raise ImportError(
|
|
"CosyVoice library not found. The bundled library is missing.\n"
|
|
"Please reinstall TTS Audio Suite or manually install CosyVoice:\n"
|
|
"1. Clone: git clone --recursive https://github.com/FunAudioLLM/CosyVoice.git\n"
|
|
"2. Install: cd CosyVoice && pip install -r requirements.txt\n"
|
|
"3. Restart ComfyUI"
|
|
)
|
|
|
|
# Verify model path
|
|
if not model_path:
|
|
raise RuntimeError("CosyVoice3 model path not provided")
|
|
|
|
if not os.path.exists(model_path):
|
|
# Try auto-download
|
|
from engines.cosyvoice.cosyvoice_downloader import cosyvoice_downloader
|
|
model_path = cosyvoice_downloader.download_model()
|
|
|
|
# Check for config file (support cosyvoice.yaml, cosyvoice2.yaml, or cosyvoice3.yaml)
|
|
config_found = False
|
|
for config_name in ["cosyvoice3.yaml", "cosyvoice2.yaml", "cosyvoice.yaml"]:
|
|
config_path = os.path.join(model_path, config_name)
|
|
if os.path.exists(config_path):
|
|
config_found = True
|
|
break
|
|
|
|
if not config_found:
|
|
raise RuntimeError(
|
|
f"CosyVoice config not found in {model_path}. "
|
|
f"Please ensure the model is correctly downloaded."
|
|
)
|
|
|
|
# Use AutoModel which automatically detects the correct model class
|
|
# This matches the official usage pattern
|
|
from cosyvoice.cli.cosyvoice import AutoModel
|
|
|
|
print(f"🔄 Loading CosyVoice model from {model_path}...")
|
|
|
|
# Handle vLLM gracefully - if not installed, just skip it
|
|
actual_load_vllm = False
|
|
if load_vllm:
|
|
try:
|
|
import vllm
|
|
actual_load_vllm = True
|
|
except ImportError:
|
|
print("⚠️ vLLM not installed, skipping vLLM optimization")
|
|
actual_load_vllm = False
|
|
|
|
if load_trt or actual_load_vllm:
|
|
print(f"🚀 Loading CosyVoice with optimizations: TensorRT={load_trt}, vLLM={actual_load_vllm}, FP16={use_fp16}")
|
|
|
|
engine = AutoModel(
|
|
model_dir=model_path,
|
|
load_trt=load_trt,
|
|
load_vllm=actual_load_vllm,
|
|
fp16=use_fp16,
|
|
llm_filename=llm_filename # Support model variants (llm.pt vs llm.rl.pt)
|
|
)
|
|
|
|
print(f"✅ CosyVoice model loaded via unified interface on {device}")
|
|
return engine
|
|
|
|
except ImportError as e:
|
|
raise ImportError(f"CosyVoice3 dependencies not available. Error: {e}")
|
|
except Exception as e:
|
|
raise RuntimeError(f"Failed to load CosyVoice3 model: {e}")
|
|
|
|
unified_model_interface.register_model_factory("cosyvoice", "tts", cosyvoice_factory)
|
|
|
|
|
|
def register_moss_tts_factory():
|
|
"""Register MOSS-TTS model factory."""
|
|
def moss_tts_factory(config: ModelLoadConfig):
|
|
"""Factory for official MOSS-TTS models with ComfyUI integration."""
|
|
import os
|
|
|
|
model_name = config.model_name or "MOSS-TTS-Local-Transformer"
|
|
model_path = config.model_path or model_name
|
|
device = config.device or "auto"
|
|
additional_params = config.additional_params or {}
|
|
dtype = additional_params.get("dtype", "auto")
|
|
attn_implementation = additional_params.get("attn_implementation", "auto")
|
|
codec_model = additional_params.get("codec_model", "MOSS-Audio-Tokenizer")
|
|
lora_adapter = additional_params.get("lora_adapter")
|
|
|
|
from engines.moss_tts.moss_tts import MossTTSEngine
|
|
from engines.moss_tts.moss_tts_downloader import MossTTSDownloader
|
|
|
|
downloader = MossTTSDownloader()
|
|
resolved_model_path = downloader.resolve_model_path(model_path)
|
|
resolved_codec_path = downloader.resolve_model_path(codec_model)
|
|
|
|
if not resolved_model_path or not os.path.exists(resolved_model_path):
|
|
raise RuntimeError(f"MOSS-TTS model not found at {resolved_model_path}")
|
|
if not resolved_codec_path or not os.path.exists(resolved_codec_path):
|
|
raise RuntimeError(f"MOSS-TTS audio tokenizer not found at {resolved_codec_path}")
|
|
|
|
print(f"🔄 Loading MOSS-TTS model via unified interface: {model_name}")
|
|
engine = MossTTSEngine(
|
|
model_path=resolved_model_path,
|
|
codec_path=resolved_codec_path,
|
|
model_variant=model_name.replace("local:", ""),
|
|
device=device,
|
|
dtype=dtype,
|
|
attn_implementation=attn_implementation,
|
|
lora_adapter=lora_adapter,
|
|
)
|
|
engine._ensure_model_loaded()
|
|
print(f"✅ MOSS-TTS model '{model_name}' loaded successfully")
|
|
return engine
|
|
|
|
unified_model_interface.register_model_factory("moss_tts", "tts", moss_tts_factory)
|
|
|
|
|
|
def register_moss_soundeffect_v2_factory():
|
|
"""Register the native MOSS-SoundEffect v2 diffusion pipeline."""
|
|
|
|
def moss_soundeffect_v2_factory(config: ModelLoadConfig):
|
|
from engines.moss_soundeffect_v2.downloader import MossSoundEffectV2Downloader
|
|
from engines.moss_soundeffect_v2.engine import MossSoundEffectV2Engine
|
|
|
|
model_name = config.model_name or MossSoundEffectV2Downloader.MODEL_NAME
|
|
model_path = config.model_path or model_name
|
|
dtype = (config.additional_params or {}).get("dtype", "auto")
|
|
resolved_model_path = MossSoundEffectV2Downloader().resolve_model_path(model_path)
|
|
|
|
engine = MossSoundEffectV2Engine(
|
|
model_path=resolved_model_path,
|
|
device=config.device or "auto",
|
|
dtype=dtype,
|
|
)
|
|
engine._ensure_model_loaded()
|
|
return engine
|
|
|
|
unified_model_interface.register_model_factory(
|
|
"moss_soundeffect_v2", "tts", moss_soundeffect_v2_factory
|
|
)
|
|
|
|
|
|
def register_qwen3_tts_factory():
|
|
"""Register Qwen3-TTS model factory"""
|
|
def qwen3_tts_factory(config: ModelLoadConfig):
|
|
"""Factory for Qwen3-TTS models with ComfyUI integration"""
|
|
import os
|
|
import sys
|
|
import importlib.metadata
|
|
import torch
|
|
|
|
# Extract parameters
|
|
model_name = config.model_name # e.g., "Qwen3-TTS-12Hz-1.7B-CustomVoice"
|
|
model_path = config.model_path or model_name
|
|
device = config.device or "auto"
|
|
|
|
# Get additional params
|
|
additional_params = config.additional_params or {}
|
|
dtype_str = additional_params.get('dtype', 'auto')
|
|
attn_implementation = additional_params.get('attn_implementation', 'auto')
|
|
|
|
# Resolve model path using downloader (handles "local:" prefix and auto-download)
|
|
from engines.qwen3_tts.qwen3_tts_downloader import Qwen3TTSDownloader
|
|
downloader = Qwen3TTSDownloader()
|
|
resolved_model_path = downloader.resolve_model_path(model_path)
|
|
|
|
if not resolved_model_path or not os.path.exists(resolved_model_path):
|
|
raise RuntimeError(f"Qwen3-TTS model not found at {resolved_model_path}. Auto-download should have been triggered earlier.")
|
|
|
|
try:
|
|
# Add bundled implementation to sys.path
|
|
qwen3_impl_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'engines', 'qwen3_tts', 'impl')
|
|
qwen3_impl_dir = os.path.abspath(qwen3_impl_dir)
|
|
if qwen3_impl_dir not in sys.path:
|
|
sys.path.insert(0, qwen3_impl_dir)
|
|
|
|
# Import bundled Qwen3-TTS implementation
|
|
from qwen_tts import Qwen3TTSModel
|
|
|
|
# Resolve torch dtype
|
|
dtype_map = {
|
|
"bfloat16": torch.bfloat16,
|
|
"float16": torch.float16,
|
|
"float32": torch.float32,
|
|
}
|
|
if dtype_str in dtype_map:
|
|
torch_dtype = dtype_map[dtype_str]
|
|
else:
|
|
# Auto mode: detect GPU compute capability
|
|
if torch.cuda.is_available():
|
|
major, minor = torch.cuda.get_device_capability()
|
|
torch_dtype = torch.bfloat16 if major >= 8 else torch.float16
|
|
else:
|
|
torch_dtype = torch.float16
|
|
|
|
# Resolve device
|
|
if device == "auto":
|
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
|
|
# Resolve attention implementation
|
|
use_sage_attn = False
|
|
if attn_implementation == "auto":
|
|
# Priority: sage_attn > flash_attention_2 > sdpa > eager
|
|
try:
|
|
from sageattention import sageattn
|
|
resolved_attn = "sage_attn"
|
|
use_sage_attn = True
|
|
print(f"[Qwen3-TTS] Auto-selected attention: sage_attn")
|
|
except ImportError:
|
|
try:
|
|
import flash_attn
|
|
# flash_attn can import but still be broken (missing metadata on some Windows installs)
|
|
importlib.metadata.version("flash_attn")
|
|
resolved_attn = "flash_attention_2"
|
|
print(f"[Qwen3-TTS] Auto-selected attention: flash_attention_2")
|
|
except Exception as flash_err:
|
|
if "flash_attn" in str(flash_err):
|
|
print(f"[Qwen3-TTS] flash_attn unavailable/incomplete, falling back to sdpa: {flash_err}")
|
|
resolved_attn = "sdpa" # PyTorch scaled dot product attention
|
|
print(f"[Qwen3-TTS] Auto-selected attention: sdpa")
|
|
elif attn_implementation == "sage_attn":
|
|
resolved_attn = "sage_attn"
|
|
use_sage_attn = True
|
|
else:
|
|
resolved_attn = attn_implementation
|
|
|
|
print(f"🔄 Loading Qwen3-TTS: {model_name}")
|
|
print(f" Path: {resolved_model_path}")
|
|
print(f" Device: {device} | Dtype: {torch_dtype} | Attention: {resolved_attn}")
|
|
|
|
# Handle sage_attn (requires post-load patching)
|
|
if use_sage_attn:
|
|
try:
|
|
from sageattention import sageattn
|
|
print(f"🔧 [Qwen3-TTS] Loading model with sage_attn (sageattention)")
|
|
|
|
# Load model without attn_implementation (will use default)
|
|
qwen3_model = Qwen3TTSModel.from_pretrained(
|
|
pretrained_model_name_or_path=resolved_model_path,
|
|
device_map=device,
|
|
dtype=torch_dtype
|
|
)
|
|
|
|
# Patch attention modules to use sageattention
|
|
patched_count = 0
|
|
for name, module in qwen3_model.model.named_modules():
|
|
# Look for attention modules
|
|
if hasattr(module, 'forward') and ('Attention' in type(module).__name__ or 'attn' in name.lower()):
|
|
try:
|
|
original_forward = module.forward
|
|
|
|
def make_sage_forward(orig_forward, mod):
|
|
def sage_forward(*args, **kwargs):
|
|
# Extract q, k, v from attention call
|
|
if len(args) >= 3:
|
|
q, k, v = args[0], args[1], args[2]
|
|
else:
|
|
return orig_forward(*args, **kwargs)
|
|
|
|
# Handle attention_mask
|
|
attn_mask = kwargs.get('attention_mask', None)
|
|
|
|
# Call sageattention
|
|
out = sageattn(q, k, v, is_causal=False, attn_mask=attn_mask)
|
|
return out
|
|
return sage_forward
|
|
|
|
module.forward = make_sage_forward(original_forward, module)
|
|
patched_count += 1
|
|
except Exception:
|
|
pass
|
|
|
|
print(f"🔧 [Qwen3-TTS] Patched {patched_count} attention modules with sage_attn")
|
|
|
|
except (ImportError, Exception) as e:
|
|
print(f"⚠️ [Qwen3-TTS] Failed with sage_attn, falling back to sdpa: {e}")
|
|
# Fallback to sdpa
|
|
qwen3_model = Qwen3TTSModel.from_pretrained(
|
|
pretrained_model_name_or_path=resolved_model_path,
|
|
device_map=device,
|
|
dtype=torch_dtype,
|
|
attn_implementation="sdpa"
|
|
)
|
|
else:
|
|
# Load with standard attention implementation
|
|
try:
|
|
qwen3_model = Qwen3TTSModel.from_pretrained(
|
|
pretrained_model_name_or_path=resolved_model_path,
|
|
device_map=device,
|
|
dtype=torch_dtype,
|
|
attn_implementation=resolved_attn
|
|
)
|
|
except Exception as e:
|
|
# Auto mode may still hit runtime flash-attn issues in some environments.
|
|
if attn_implementation == "auto" and resolved_attn == "flash_attention_2" and "flash_attn" in str(e):
|
|
print(f"⚠️ [Qwen3-TTS] Failed to load with flash_attention_2, retrying with sdpa: {e}")
|
|
qwen3_model = Qwen3TTSModel.from_pretrained(
|
|
pretrained_model_name_or_path=resolved_model_path,
|
|
device_map=device,
|
|
dtype=torch_dtype,
|
|
attn_implementation="sdpa"
|
|
)
|
|
else:
|
|
raise
|
|
|
|
print(f"✅ Qwen3-TTS model '{model_name}' loaded successfully")
|
|
|
|
# Return the bundled model directly
|
|
return qwen3_model
|
|
|
|
except ImportError as e:
|
|
raise ImportError(f"Qwen3-TTS bundled implementation not available. Error: {e}")
|
|
except Exception as e:
|
|
raise RuntimeError(f"Failed to load Qwen3-TTS model: {e}")
|
|
|
|
unified_model_interface.register_model_factory("qwen3_tts", "tts", qwen3_tts_factory)
|
|
|
|
|
|
def register_fish_audio_s2_factory():
|
|
"""Register Fish S2; isolated routing constructs the actual proxy."""
|
|
def fish_audio_s2_factory(config: ModelLoadConfig):
|
|
return build_fish_audio_s2_proxy(config)
|
|
unified_model_interface.register_model_factory("fish_audio_s2", "tts", fish_audio_s2_factory)
|
|
|
|
|
|
def register_dots_tts_factory():
|
|
"""Register Dots TTS model factory."""
|
|
def dots_tts_factory(config: ModelLoadConfig):
|
|
"""Factory for official Dots TTS models with ComfyUI integration."""
|
|
model_name = config.model_name or "dots.tts-soar"
|
|
device = config.device or "auto"
|
|
additional_params = config.additional_params or {}
|
|
precision = additional_params.get("precision", "auto")
|
|
optimize = bool(additional_params.get("optimize", False))
|
|
max_generate_length = int(additional_params.get("max_generate_length", 500))
|
|
|
|
from engines.dots_tts.dots_tts_engine import DotsTTSEngine
|
|
|
|
print(f"🔄 Loading Dots TTS model via unified interface: {model_name}")
|
|
engine = DotsTTSEngine(
|
|
model_name=model_name,
|
|
device=device,
|
|
precision=precision,
|
|
optimize=optimize,
|
|
max_generate_length=max_generate_length,
|
|
)
|
|
engine._ensure_runtime_loaded()
|
|
print(f"✅ Dots TTS model '{model_name}' loaded successfully")
|
|
return engine
|
|
|
|
unified_model_interface.register_model_factory("dots_tts", "tts", dots_tts_factory)
|
|
|
|
|
|
def register_dramabox_factory():
|
|
"""Register official DramaBox in the main Transformers 5 environment."""
|
|
def dramabox_factory(config: ModelLoadConfig):
|
|
from engines.dramabox.dramabox_downloader import DramaBoxDownloader
|
|
from engines.dramabox.dramabox_engine import DramaBoxEngine
|
|
|
|
model_name = config.model_name or DramaBoxDownloader.MODEL_NAME
|
|
device = config.device or "auto"
|
|
additional_params = config.additional_params or {}
|
|
precision = additional_params.get("precision", "auto")
|
|
model_paths = DramaBoxDownloader().resolve_model_path(model_name)
|
|
|
|
print(f"🔄 Loading DramaBox via unified interface: {model_name}")
|
|
engine = DramaBoxEngine(
|
|
model_name=model_name,
|
|
device=device,
|
|
precision=precision,
|
|
model_paths=model_paths,
|
|
memory_mode=additional_params.get("memory_mode", "fast"),
|
|
transformer_quantization=additional_params.get(
|
|
"transformer_quantization", "none"
|
|
),
|
|
compile_model=bool(additional_params.get("compile_model", False)),
|
|
)
|
|
engine._ensure_runtime_loaded()
|
|
print(f"✅ DramaBox model '{model_name}' loaded successfully")
|
|
return engine
|
|
|
|
unified_model_interface.register_model_factory("dramabox", "tts", dramabox_factory)
|
|
|
|
|
|
def register_omnivoice_factory():
|
|
"""Register OmniVoice model factory."""
|
|
|
|
def omnivoice_factory(config: ModelLoadConfig):
|
|
"""Factory for official OmniVoice models with ComfyUI integration."""
|
|
model_name = config.model_name or "OmniVoice"
|
|
device = config.device or "auto"
|
|
additional_params = config.additional_params or {}
|
|
dtype = additional_params.get("dtype", "auto")
|
|
|
|
from engines.omnivoice.omnivoice_engine import OmniVoiceEngine
|
|
|
|
print(f"🔄 Loading OmniVoice model via unified interface: {model_name}")
|
|
engine = OmniVoiceEngine(
|
|
model_name=model_name,
|
|
device=device,
|
|
dtype=dtype,
|
|
)
|
|
engine._ensure_model_loaded()
|
|
print(f"✅ OmniVoice model '{model_name}' loaded successfully")
|
|
return engine
|
|
|
|
unified_model_interface.register_model_factory("omnivoice", "tts", omnivoice_factory)
|
|
|
|
|
|
def register_qwen3_asr_factory():
|
|
"""Register Qwen3-ASR model factory"""
|
|
def qwen3_asr_factory(config: ModelLoadConfig):
|
|
"""Factory for Qwen3-ASR models with ComfyUI integration"""
|
|
model_name = config.model_name or "Qwen3-ASR-1.7B"
|
|
model_path = config.model_path or model_name
|
|
device = config.device or "auto"
|
|
|
|
additional_params = config.additional_params or {}
|
|
precision = additional_params.get("precision", "auto")
|
|
attn_implementation = additional_params.get("attn_implementation", "sdpa")
|
|
max_new_tokens = int(additional_params.get("max_new_tokens", 256))
|
|
forced_aligner = additional_params.get("forced_aligner")
|
|
forced_aligner_kwargs = additional_params.get("forced_aligner_kwargs")
|
|
|
|
from engines.qwen3_tts.qwen3_asr_downloader import Qwen3ASRDownloader
|
|
downloader = Qwen3ASRDownloader()
|
|
resolved_model_path = downloader.resolve_model_path(model_path)
|
|
|
|
torch_dtype = _resolve_torch_dtype(precision)
|
|
|
|
if device == "auto":
|
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
torch_dtype, attn_implementation = _sanitize_loader_settings(device, torch_dtype, attn_implementation)
|
|
|
|
try:
|
|
from qwen_asr import Qwen3ASRModel
|
|
except ImportError:
|
|
try:
|
|
import sys
|
|
import os
|
|
qwen3_asr_impl = os.path.join(os.path.dirname(__file__), "..", "..", "engines", "qwen3_asr", "impl")
|
|
qwen3_asr_impl = os.path.abspath(qwen3_asr_impl)
|
|
if qwen3_asr_impl not in sys.path:
|
|
sys.path.insert(0, qwen3_asr_impl)
|
|
from qwen_asr import Qwen3ASRModel
|
|
except Exception as e:
|
|
raise ImportError(f"Qwen3-ASR dependency not available. Error: {e}")
|
|
|
|
loader_kwargs = {
|
|
"pretrained_model_name_or_path": resolved_model_path,
|
|
"dtype": torch_dtype,
|
|
"device_map": device,
|
|
"max_new_tokens": max_new_tokens,
|
|
"attn_implementation": attn_implementation,
|
|
}
|
|
|
|
if forced_aligner:
|
|
loader_kwargs["forced_aligner"] = downloader.resolve_model_path(forced_aligner)
|
|
loader_kwargs["forced_aligner_kwargs"] = forced_aligner_kwargs or {
|
|
"dtype": torch_dtype,
|
|
"device_map": device,
|
|
"attn_implementation": attn_implementation,
|
|
}
|
|
|
|
print(f"🔄 Loading Qwen3-ASR: {model_name}")
|
|
print(f" Path: {resolved_model_path}")
|
|
print(f" Device: {device} | Dtype: {torch_dtype} | Attention: {attn_implementation}")
|
|
|
|
model = Qwen3ASRModel.from_pretrained(**loader_kwargs)
|
|
print(f"✅ Qwen3-ASR model '{model_name}' loaded successfully")
|
|
return model
|
|
|
|
unified_model_interface.register_model_factory("qwen3_asr", "asr", qwen3_asr_factory)
|
|
|
|
|
|
def register_qwen3_aligner_factory():
|
|
"""Register standalone Qwen3 forced aligner factory."""
|
|
|
|
def qwen3_aligner_factory(config: ModelLoadConfig):
|
|
model_name = config.model_name or "Qwen3-ForcedAligner-0.6B"
|
|
model_path = config.model_path or model_name
|
|
device = config.device or "auto"
|
|
|
|
additional_params = config.additional_params or {}
|
|
precision = additional_params.get("precision", "auto")
|
|
attn_implementation = additional_params.get("attn_implementation", "sdpa")
|
|
|
|
from engines.qwen3_tts.qwen3_asr_downloader import Qwen3ASRDownloader
|
|
downloader = Qwen3ASRDownloader()
|
|
resolved_model_path = downloader.resolve_model_path(model_path)
|
|
|
|
torch_dtype = _resolve_torch_dtype(precision)
|
|
|
|
if device == "auto":
|
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
torch_dtype, resolved_attn = _sanitize_loader_settings(device, torch_dtype, attn_implementation)
|
|
|
|
try:
|
|
from qwen_asr import Qwen3ForcedAligner
|
|
except ImportError:
|
|
try:
|
|
import sys
|
|
import os
|
|
qwen3_asr_impl = os.path.join(os.path.dirname(__file__), "..", "..", "engines", "qwen3_asr", "impl")
|
|
qwen3_asr_impl = os.path.abspath(qwen3_asr_impl)
|
|
if qwen3_asr_impl not in sys.path:
|
|
sys.path.insert(0, qwen3_asr_impl)
|
|
from qwen_asr import Qwen3ForcedAligner
|
|
except Exception as e:
|
|
raise ImportError(f"Qwen3 forced aligner dependency not available. Error: {e}")
|
|
|
|
print(f"🔄 Loading Qwen3 forced aligner: {model_name}")
|
|
print(f" Path: {resolved_model_path}")
|
|
print(f" Device: {device} | Dtype: {torch_dtype} | Attention: {resolved_attn}")
|
|
|
|
aligner = Qwen3ForcedAligner.from_pretrained(
|
|
resolved_model_path,
|
|
device_map=device,
|
|
dtype=torch_dtype,
|
|
attn_implementation=resolved_attn,
|
|
)
|
|
print(f"✅ Qwen3 forced aligner '{model_name}' loaded successfully")
|
|
return aligner
|
|
|
|
unified_model_interface.register_model_factory("qwen3_asr", "aligner", qwen3_aligner_factory)
|
|
|
|
|
|
def register_granite_asr_factory():
|
|
"""Register Granite ASR model factory."""
|
|
|
|
def granite_asr_factory(config: ModelLoadConfig):
|
|
from packaging import version
|
|
import transformers
|
|
from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor
|
|
|
|
if version.parse(transformers.__version__) < version.parse("4.52.1"):
|
|
raise RuntimeError(
|
|
f"Granite ASR requires transformers>=4.52.1, found {transformers.__version__}"
|
|
)
|
|
|
|
model_name = config.model_name or "granite-4.0-1b-speech"
|
|
model_path = config.model_path or model_name
|
|
device = config.device or "auto"
|
|
|
|
additional_params = config.additional_params or {}
|
|
precision = additional_params.get("precision", "auto")
|
|
attn_implementation = additional_params.get("attn_implementation", "auto")
|
|
|
|
from engines.granite_asr.granite_asr_downloader import GraniteASRDownloader
|
|
from engines.granite_asr.runtime import GraniteASRRuntime
|
|
|
|
downloader = GraniteASRDownloader()
|
|
resolved_model_path = downloader.resolve_model_path(model_path)
|
|
|
|
torch_dtype = _resolve_torch_dtype(precision)
|
|
|
|
if device == "auto":
|
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
torch_dtype, resolved_attn = _sanitize_loader_settings(device, torch_dtype, attn_implementation)
|
|
|
|
print(f"🔄 Loading Granite ASR: {model_name}")
|
|
print(f" Path: {resolved_model_path}")
|
|
print(f" Device: {device} | Dtype: {torch_dtype} | Attention: {resolved_attn}")
|
|
|
|
processor = AutoProcessor.from_pretrained(resolved_model_path)
|
|
|
|
def _load_granite_model(attn_impl: str):
|
|
return AutoModelForSpeechSeq2Seq.from_pretrained(
|
|
resolved_model_path,
|
|
device_map=device,
|
|
dtype=torch_dtype,
|
|
attn_implementation=attn_impl,
|
|
)
|
|
|
|
try:
|
|
model = _load_granite_model(resolved_attn)
|
|
except Exception as e:
|
|
error_text = str(e)
|
|
needs_eager_fallback = (
|
|
resolved_attn != "eager"
|
|
and "Blip2QFormerModel does not support an attention implementation" in error_text
|
|
)
|
|
if not needs_eager_fallback:
|
|
raise
|
|
|
|
print(
|
|
f"⚠️ Granite ASR attention backend '{resolved_attn}' is not supported by the bundled BLIP2 Q-Former in this transformers build."
|
|
)
|
|
print(" Falling back to eager attention for Granite ASR.")
|
|
model = _load_granite_model("eager")
|
|
|
|
runtime = GraniteASRRuntime(
|
|
model=model,
|
|
processor=processor,
|
|
device=device,
|
|
)
|
|
print(f"✅ Granite ASR model '{model_name}' loaded successfully")
|
|
return runtime
|
|
|
|
unified_model_interface.register_model_factory("granite_asr", "asr", granite_asr_factory)
|
|
|
|
|
|
def initialize_all_factories():
|
|
"""Initialize all model factories"""
|
|
register_chatterbox_factory()
|
|
register_chatterbox_23lang_factory()
|
|
register_f5tts_factory()
|
|
register_step_audio_editx_factory()
|
|
register_echo_tts_factory()
|
|
register_higgs_audio_factory()
|
|
register_higgs_audio_v3_factory()
|
|
register_rvc_factory()
|
|
register_vibevoice_factory()
|
|
register_index_tts_factory()
|
|
register_cosyvoice_factory()
|
|
register_moss_tts_factory()
|
|
register_moss_soundeffect_v2_factory()
|
|
register_qwen3_tts_factory()
|
|
register_fish_audio_s2_factory()
|
|
register_dots_tts_factory()
|
|
register_dramabox_factory()
|
|
register_omnivoice_factory()
|
|
register_qwen3_asr_factory()
|
|
register_qwen3_aligner_factory()
|
|
register_granite_asr_factory()
|
|
|
|
|
|
# Auto-initialize on import
|
|
initialize_all_factories()
|
|
|
|
|
|
# Standalone helper functions for adapters
|
|
def get_cached_model(engine_name: str, model_type: str = "tts"):
|
|
"""
|
|
Get cached model by engine name and type.
|
|
|
|
Args:
|
|
engine_name: Engine name (e.g., "vibevoice", "step_audio_editx")
|
|
model_type: Model type (default: "tts")
|
|
|
|
Returns:
|
|
Cached model wrapper or None if not found
|
|
"""
|
|
interface = UnifiedModelInterface()
|
|
return interface.get_cached_model(engine_name, model_type)
|