Files
diodiogod-TTS-Audio-Suite/utils/models/unified_model_interface.py
2026-08-11 23:08:35 -03:00

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)