#!/usr/bin/env python3 """ TTS Audio Suite - ComfyUI Installation Script Handles Python 3.13 compatibility and dependency conflicts automatically. This script is called by ComfyUI Manager to install all required dependencies for the TTS Audio Suite custom node with proper conflict resolution. NOTE: torchcodec (audio codec library) was removed to eliminate FFmpeg system dependency. torchaudio.load() works fine with fallback backends (soundfile, scipy). No quality loss - only negligible audio loading speed difference. """ import subprocess import sys import os import platform import importlib.machinery import importlib.util import hashlib import json import shutil import tempfile from pathlib import Path from typing import List, Optional class TTSAudioInstaller: """Intelligent installer for TTS Audio Suite with Python 3.13 compatibility""" # Bump when the install procedure itself gains or changes dependencies. # This keeps normal ComfyUI patch updates on the fast path. INSTALL_STATE_VERSION = 2 CORE_MODULE_CHECKS = ( ("torch", "PyTorch"), ("torchaudio", "TorchAudio"), ("transformers", "Transformers"), ("soundfile", "SoundFile"), ("numpy", "NumPy"), ("librosa", "Librosa"), ("omegaconf", "OmegaConf"), ) # These are the external engine runtimes installed or repaired by this # script. Imports only verify package availability; they never load models. ENGINE_RUNTIME_CHECKS = ( ("VibeVoice", ("vibevoice", "av")), ("Echo-TTS", ("echo_tts",)), ("OmniVoice", ("omnivoice",)), ("Dots TTS", ("dots_tts.runtime",)), ("DramaBox", ("av", "einops", "yaml")), ("Fish Audio S2", ("fish_speech.inference_engine",)), ) def __init__(self): self.python_version = sys.version_info self.is_python_313 = self.python_version >= (3, 13) self.is_windows = platform.system() == "Windows" self.is_macos = platform.system() == "Darwin" self.is_m1_mac = self.is_macos and platform.machine() == "arm64" self.pip_cmd = [sys.executable, "-m", "pip"] self.russian_stress_fork_ref = "git+https://github.com/diodiogod/add-stress-to-epub.git@98f53b9" self.engine_validation_results = {} self.install_warnings = [] def log(self, message: str, level: str = "INFO"): """Log installation progress with safe visual indicators""" # Use ASCII-safe symbols that work on all systems symbol_map = { "INFO": "[i]", "SUCCESS": "[+]", "WARNING": "[!]", "ERROR": "[X]", "INSTALL": "[*]" } symbol = symbol_map.get(level, "[i]") print(f"{symbol} {message}") def add_install_warning(self, description: str, detail: str): """Record one concise warning without duplicating it in the summary.""" if not any(existing[0] == description for existing in self.install_warnings): self.install_warnings.append((description, detail)) def ensure_requirements_installed(self): """Check and install requirements.txt if needed""" self.log("Checking requirements.txt dependencies", "INFO") requirements_path = os.path.join(os.path.dirname(__file__), "requirements.txt") if not os.path.exists(requirements_path): self.log("requirements.txt not found - skipping", "WARNING") return # Parse requirements.txt to get all package specs and detect both missing and incompatible versions. packages_to_install = [] package_specs = {} try: try: from importlib.metadata import version, PackageNotFoundError except ImportError: from importlib_metadata import version, PackageNotFoundError try: from packaging.requirements import Requirement from packaging.utils import canonicalize_name except Exception: Requirement = None canonicalize_name = None with open(requirements_path, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if line and not line.startswith('#'): # Remove inline comments clean_line = line.split('#')[0].strip() if not clean_line: continue # Parse requirement with markers if possible if Requirement: try: req = Requirement(clean_line) except Exception: req = None else: req = None if req and req.marker and not req.marker.evaluate(): continue if req: package_name = req.name else: # Fallback: extract package name (before >= <= == etc.) package_name = clean_line.split('>=')[0].split('<=')[0].split('==')[0].split('<')[0].split('>')[0].split('!')[0].strip() if not package_name: continue normalized_name = canonicalize_name(package_name) if canonicalize_name else package_name package_specs[normalized_name] = clean_line try: installed_version = version(package_name) if req and req.specifier and installed_version not in req.specifier: packages_to_install.append(package_name) except PackageNotFoundError: packages_to_install.append(package_name) except Exception as e: self.log(f"Error reading requirements.txt: {e}", "WARNING") return # Preserve order but avoid duplicate installs if multiple requirement lines resolve to the same package. deduped_packages_to_install = [] seen = set() for package in packages_to_install: normalized_name = canonicalize_name(package) if canonicalize_name else package if normalized_name in seen: continue seen.add(normalized_name) deduped_packages_to_install.append(package) if deduped_packages_to_install: self.log( f"Found {len(deduped_packages_to_install)} missing/outdated requirements.txt packages: " f"{', '.join(deduped_packages_to_install[:5])}" f"{'...' if len(deduped_packages_to_install) > 5 else ''}", "WARNING" ) self.log("Installing/upgrading requirements individually (preserves ComfyUI Manager safeguards)", "INFO") # Install each missing/incompatible package individually using our safe method for package in deduped_packages_to_install: normalized_name = canonicalize_name(package) if canonicalize_name else package package_spec = package_specs.get(normalized_name, package) self.run_pip_command(["install", package_spec], f"Installing {package}", ignore_errors=True) else: self.log("All requirements.txt dependencies already satisfied", "SUCCESS") def check_system_dependencies(self): """Check optional system libraries and provide helpful feature warnings.""" if self.is_windows: return True # Windows packages come pre-compiled if self.is_macos: return self.check_macos_dependencies() else: return self.check_linux_dependencies() def _find_macos_library(self, lib_name): """Find library on macOS, checking both system paths and Homebrew installations""" import ctypes.util import glob # First try standard ctypes.util.find_library (works for system libraries) if ctypes.util.find_library(lib_name): return True # Fallback: Check Homebrew installation paths directly # Apple Silicon Macs: /opt/homebrew/lib # Intel Macs: /usr/local/lib homebrew_paths = ['/opt/homebrew/lib', '/usr/local/lib'] for homebrew_path in homebrew_paths: # Look for .dylib files matching the library name pattern = f"{homebrew_path}/lib{lib_name}*.dylib" matches = glob.glob(pattern) if matches: return True return False def check_macos_dependencies(self): """Check for optional system libraries on macOS.""" self.log("Checking macOS system dependencies...", "INFO") missing_deps = [] # Check for libsamplerate (needed by resampy/soxr) try: if not self._find_macos_library('samplerate'): missing_deps.append(('libsamplerate', 'audio resampling')) except Exception: pass # Check for portaudio (needed for sounddevice) try: if not self._find_macos_library('portaudio'): missing_deps.append(('portaudio', 'voice recording')) except Exception: pass if missing_deps: self.log("Optional system dependencies are missing", "WARNING") print("\n" + "="*60) print("OPTIONAL MACOS SYSTEM DEPENDENCIES") print("="*60) for dep, purpose in missing_deps: print(f"• {dep} (for {purpose})") print("\nPlease install with Homebrew:") deps_list = " ".join([dep for dep, _ in missing_deps]) print(f"brew install {deps_list}") print("\n# If you don't have Homebrew:") print('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"') if self.is_m1_mac: print("\n# M1/M2 Mac Note:") print("Make sure you're using an ARM64 Python environment!") print("Check with: python -c \"import platform; print(platform.machine())\"") print("Should show: arm64 (not x86_64)") print("="*60) print("Core TTS installation will continue; only the listed features may be unavailable.\n") self.add_install_warning( "Optional macOS audio libraries", "Install the listed Homebrew packages to enable every audio feature.", ) if not missing_deps: self.log("macOS system dependencies check passed", "SUCCESS") return True def check_linux_dependencies(self): """Check for optional system libraries on Linux.""" self.log("Checking Linux system dependencies...", "INFO") missing_deps = [] # Check for libsamplerate (needed by resampy/soxr) try: # Try importing a package that would fail if libsamplerate is missing import ctypes.util if not ctypes.util.find_library('samplerate'): missing_deps.append(('libsamplerate0-dev', 'audio resampling')) except: pass # Check for portaudio (needed for sounddevice) try: import ctypes.util if not ctypes.util.find_library('portaudio'): missing_deps.append(('portaudio19-dev', 'voice recording')) except: pass if missing_deps: self.log("Optional system dependencies are missing", "WARNING") print("\n" + "="*60) print("OPTIONAL LINUX SYSTEM DEPENDENCIES") print("="*60) for dep, purpose in missing_deps: print(f"• {dep} (for {purpose})") print("\nPlease install with:") print("# Ubuntu/Debian:") deps_list = " ".join([dep for dep, _ in missing_deps]) print(f"sudo apt-get install {deps_list}") print("\n# Fedora/RHEL:") fedora_packages = { "libsamplerate0-dev": "libsamplerate-devel", "portaudio19-dev": "portaudio-devel", } fedora_deps = " ".join(fedora_packages.get(dep, dep) for dep, _ in missing_deps) print(f"sudo dnf install {fedora_deps}") print("="*60) print("Core TTS installation will continue; only the listed features may be unavailable.\n") self.add_install_warning( "Optional Linux audio libraries", "Install the listed system packages to enable every audio feature.", ) if not missing_deps: self.log("Linux system dependencies check passed", "SUCCESS") return True def run_pip_command(self, args: List[str], description: str, ignore_errors: bool = False) -> bool: """Execute pip command with error handling and Windows UTF-8 support""" cmd = self.pip_cmd + args self.log(f"{description}...", "INSTALL") # Set UTF-8 encoding for Windows to prevent charmap errors env = os.environ.copy() if self.is_windows: env['PYTHONUTF8'] = '1' env['PYTHONIOENCODING'] = 'utf-8' try: result = subprocess.run( cmd, capture_output=True, text=True, check=True, env=env, encoding='utf-8' if self.is_windows else None ) if result.stdout.strip(): print(result.stdout) return True except subprocess.CalledProcessError as e: if ignore_errors: error_msg = e.stderr.strip() if len(error_msg) > 1000: error_msg = f"...(last 1000 chars)...\n{error_msg[-1000:]}" self.install_warnings.append((description, error_msg)) self.log(f"Warning: {description} failed (continuing anyway): {error_msg}", "WARNING") return False else: self.log(f"Error: {description} failed: {e.stderr.strip()}", "ERROR") raise def detect_cuda_version(self): """Detect CUDA version and determine best PyTorch index""" try: # Try to detect CUDA version result = subprocess.run(['nvidia-smi'], capture_output=True, text=True, timeout=10) if result.returncode == 0 and 'CUDA Version:' in result.stdout: # Extract CUDA version (e.g., "CUDA Version: 12.1") import re cuda_match = re.search(r'CUDA Version:\s*(\d+)\.(\d+)', result.stdout) if cuda_match: major, minor = int(cuda_match.group(1)), int(cuda_match.group(2)) self.log(f"Detected CUDA {major}.{minor}", "INFO") # Choose appropriate PyTorch CUDA build based on detected version if major == 12 and minor >= 8: return "cu124" # CUDA 12.8+ → use cu124 index elif major >= 12: return "cu121" # CUDA 12.1+ compatible elif major == 11 and minor >= 8: return "cu118" # CUDA 11.8+ compatible else: self.log(f"CUDA {major}.{minor} detected - may need manual PyTorch installation", "WARNING") return "cu118" # Fallback for older CUDA except: pass # No CUDA detected - check for AMD GPU (basic detection) try: if self.is_windows: # Windows: check for AMD in device manager output result = subprocess.run(['wmic', 'path', 'win32_VideoController', 'get', 'name'], capture_output=True, text=True, timeout=5) if 'amd' in result.stdout.lower() or 'radeon' in result.stdout.lower(): self.log("AMD GPU detected - will install CPU version (ROCm not yet supported)", "WARNING") return "cpu" except: pass self.log("No CUDA detected - installing CPU-only PyTorch", "WARNING") return "cpu" def check_pytorch_compatibility(self): """Check if current PyTorch meets version and CUDA requirements""" try: import torch current_version = torch.__version__ # Parse version (e.g., "2.5.1+cu124" -> (2, 5, 1)) # Strip CUDA suffix first to avoid parsing errors import re clean_version = current_version.split('+')[0] # Remove CUDA suffix like "+cu124" version_match = re.match(r'(\d+)\.(\d+)\.(\d+)', clean_version) if version_match: major, minor, patch = map(int, version_match.groups()) version_tuple = (major, minor, patch) # Check CUDA availability if we detected CUDA cuda_available = torch.cuda.is_available() detected_cuda = self.detect_cuda_version() != "cpu" # If CUDA mismatch, we need to reinstall if detected_cuda and not cuda_available: self.log(f"PyTorch {current_version} found but no CUDA support - will reinstall with CUDA", "WARNING") return False elif not detected_cuda and cuda_available: self.log(f"PyTorch {current_version} has unnecessary CUDA support - keeping anyway", "INFO") return True # Check security requirement: 2.6.0+ preferred for CVE-2025-32434, but 2.5.1+ acceptable if version_tuple >= (2, 6, 0): self.log(f"PyTorch {current_version} >= 2.6.0 - secure version, skipping installation", "SUCCESS") return True elif version_tuple >= (2, 5, 1): # 2.5.1+ is acceptable but try to upgrade to 2.6.0 if available self.log(f"PyTorch {current_version} >= 2.5.1 - will attempt upgrade to 2.6.0 for security fix", "INFO") return False # Try upgrade, but won't fail if 2.6.0 unavailable else: self.log(f"PyTorch {current_version} < 2.5.1 - will upgrade for security and compatibility", "WARNING") return False else: self.log(f"Could not parse PyTorch version: {current_version} - will reinstall", "WARNING") return False except ImportError: self.log("PyTorch not found - will install", "INFO") return False except Exception as e: self.log(f"Error checking PyTorch: {e} - will reinstall", "WARNING") return False def install_pytorch_with_cuda(self): """Install PyTorch with appropriate acceleration (2.6+ required for CVE-2025-32434 security fix)""" # Check if current PyTorch is already compatible if self.check_pytorch_compatibility(): return # Skip installation cuda_version = self.detect_cuda_version() if cuda_version == "cpu": self.log("Installing PyTorch 2.6+ (CPU-only)", "INFO") index_url = "https://download.pytorch.org/whl/cpu" else: self.log(f"Installing PyTorch 2.6+ with CUDA {cuda_version} support", "INFO") index_url = f"https://download.pytorch.org/whl/{cuda_version}" # Force uninstall if we need to switch between CPU/CUDA variants try: import torch current_version = torch.__version__ if (cuda_version != "cpu" and not torch.cuda.is_available()) or \ (cuda_version == "cpu" and torch.cuda.is_available()): self.log(f"Uninstalling existing PyTorch {current_version} to switch variants", "WARNING") uninstall_cmd = ["uninstall", "-y", "torch", "torchvision", "torchaudio"] self.run_pip_command(uninstall_cmd, "Uninstalling existing PyTorch") except ImportError: pass # PyTorch not installed # Try PyTorch 2.6+ first, fallback to 2.5+ if unavailable try: if cuda_version == "cpu": pytorch_packages_26 = [ "torch>=2.6.0", "torchvision", "torchaudio>=2.6.0" ] else: pytorch_packages_26 = [ "torch>=2.6.0", "torchvision", "torchaudio>=2.6.0" ] pytorch_cmd_26 = [ "install", "--upgrade", "--force-reinstall" ] + pytorch_packages_26 + [ "--index-url", index_url ] self.run_pip_command(pytorch_cmd_26, f"Installing PyTorch 2.6+ ({cuda_version} support)") except subprocess.CalledProcessError: # PyTorch 2.6.0 not available for this CUDA version - try 2.5+ self.log(f"PyTorch 2.6.0 not available for {cuda_version} - falling back to latest 2.5.x", "WARNING") if cuda_version == "cpu": pytorch_packages_25 = [ "torch>=2.5.0", "torchvision", "torchaudio>=2.5.0" ] else: pytorch_packages_25 = [ "torch>=2.5.0", "torchvision", "torchaudio>=2.5.0" ] pytorch_cmd_25 = [ "install", "--upgrade", "--force-reinstall" ] + pytorch_packages_25 + [ "--index-url", index_url ] self.run_pip_command(pytorch_cmd_25, f"Installing PyTorch 2.5+ ({cuda_version} support)") def module_available(self, module_name: str) -> bool: """Check module presence without starting another Python process or importing it.""" try: parts = module_name.split(".") spec = importlib.util.find_spec(parts[0]) if spec is None: return False # util.find_spec() imports the parent when given a dotted name. # Walk the package paths directly so presence checks stay side-effect free. for index in range(1, len(parts)): search_locations = spec.submodule_search_locations if search_locations is None: return False qualified_name = ".".join(parts[: index + 1]) spec = importlib.machinery.PathFinder.find_spec( qualified_name, search_locations, ) if spec is None: return False return True except (ImportError, ModuleNotFoundError, AttributeError, ValueError): return False def _install_state_path(self) -> Path: """Return the ignored, per-node state file used for idempotent updates.""" return Path(__file__).resolve().parent / ".cache" / "install_state.json" def _install_state_fingerprint(self) -> str: """Fingerprint installer logic and the active dependency environment.""" digest = hashlib.sha256() digest.update(str(self.INSTALL_STATE_VERSION).encode("utf-8")) digest.update(str(sys.executable).lower().encode("utf-8")) digest.update(f"{self.python_version.major}.{self.python_version.minor}".encode("utf-8")) digest.update(repr(self.ENGINE_RUNTIME_CHECKS).encode("utf-8")) # An installer fix must be able to invalidate an old successful state # without requiring users to know an environment variable. digest.update(Path(__file__).resolve().read_bytes()) requirements_path = Path(__file__).resolve().parent / "requirements.txt" if requirements_path.exists(): digest.update(requirements_path.read_bytes()) try: from importlib.metadata import PackageNotFoundError, version import re for line in requirements_path.read_text(encoding="utf-8").splitlines(): requirement = line.split("#", 1)[0].strip() if not requirement: continue package_name = re.split(r"[<>=!~;\s]", requirement, maxsplit=1)[0] try: installed_version = version(package_name) except PackageNotFoundError: installed_version = "" digest.update(f"{package_name}={installed_version}".encode("utf-8")) except (OSError, ImportError): digest.update(b"") return digest.hexdigest() def _quick_installation_status(self): """Check the small set of entry points needed to trust the install marker.""" missing = [] for module_name, display_name in self.CORE_MODULE_CHECKS: if not self.module_available(module_name): missing.append(display_name) self.engine_validation_results = {} for display_name, module_names in self.ENGINE_RUNTIME_CHECKS: available = all(self.module_available(module_name) for module_name in module_names) self.engine_validation_results[display_name] = available if not available: missing.append(display_name) return missing def can_skip_dependency_installation(self) -> bool: """Avoid rerunning pip and engine setup when this environment is unchanged.""" if os.environ.get("TTS_AUDIO_SUITE_FORCE_INSTALL") == "1": self.log("Forced installation requested by TTS_AUDIO_SUITE_FORCE_INSTALL", "WARNING") return False state_path = self._install_state_path() try: state = json.loads(state_path.read_text(encoding="utf-8")) except (FileNotFoundError, OSError, json.JSONDecodeError): return False if state.get("fingerprint") != self._install_state_fingerprint(): return False missing = self._quick_installation_status() if missing: self.log( f"Install state found, but runtime checks need repair: {', '.join(missing)}", "WARNING", ) return False self.log("Dependencies unchanged and runtime entry points are present - skipping package installation", "SUCCESS") return True def save_installation_state(self, installation_valid: bool): """Persist only a successful dependency state for future fast updates.""" if not installation_valid: return state_path = self._install_state_path() try: state_path.parent.mkdir(parents=True, exist_ok=True) state_path.write_text( json.dumps( { "fingerprint": self._install_state_fingerprint(), "python": sys.executable, "engine_runtimes": list(self.engine_validation_results), }, indent=2, ), encoding="utf-8", ) except OSError as error: self.log(f"Could not save installer state (continuing): {error}", "WARNING") def verify_faiss_installation(self) -> bool: """Verify FAISS imports and exposes the CPU APIs required by RVC index building.""" probe = ( "import json\n" "import faiss\n" "details = {\n" " 'file': getattr(faiss, '__file__', None),\n" " 'loader': type(getattr(faiss, '__loader__', None)).__name__ if getattr(faiss, '__loader__', None) else None,\n" " 'has_index_factory': hasattr(faiss, 'index_factory'),\n" " 'has_write_index': hasattr(faiss, 'write_index'),\n" " 'has_read_index': hasattr(faiss, 'read_index'),\n" "}\n" "missing = [name for name in ('index_factory', 'write_index', 'read_index') if not hasattr(faiss, name)]\n" "if missing:\n" " raise RuntimeError(f\"faiss imported but missing required API: {', ' .join(missing)} | details={details}\")\n" "print(json.dumps(details))\n" ) try: result = subprocess.run( [sys.executable, "-c", probe], capture_output=True, text=True, timeout=30, ) if result.returncode == 0: details = (result.stdout or "").strip() if details: self.log(f"FAISS verification passed: {details}", "SUCCESS") return True error_text = (result.stderr or result.stdout or "unknown faiss failure").strip() self.log(f"FAISS verification failed: {error_text}", "WARNING") return False except Exception as e: self.log(f"FAISS verification failed: {e}", "WARNING") return False def cleanup_stale_faiss_namespace(self) -> None: """Remove clearly broken namespace-only faiss directories left behind by bad installs.""" probe = ( "import json, shutil, site, sysconfig\n" "from pathlib import Path\n" "roots = set()\n" "for base in list(site.getsitepackages()) + [site.getusersitepackages(), sysconfig.get_paths().get('purelib'), sysconfig.get_paths().get('platlib')]:\n" " if base:\n" " roots.add(base)\n" "removed = []\n" "for root in roots:\n" " candidate = Path(root) / 'faiss'\n" " if not candidate.is_dir():\n" " continue\n" " has_init = (candidate / '__init__.py').exists()\n" " has_binary = any(candidate.glob('*.so')) or any(candidate.glob('*.pyd')) or any(candidate.glob('*.dll')) or any(candidate.glob('*.dylib'))\n" " if not has_init and not has_binary:\n" " shutil.rmtree(candidate, ignore_errors=True)\n" " removed.append(str(candidate))\n" "print(json.dumps(removed))\n" ) try: result = subprocess.run( [sys.executable, "-c", probe], capture_output=True, text=True, timeout=30, ) if result.returncode != 0: error_text = (result.stderr or result.stdout or "unknown cleanup failure").strip() self.log(f"FAISS namespace cleanup failed: {error_text}", "WARNING") return removed = (result.stdout or "[]").strip() if removed and removed != "[]": self.log(f"Removed stale namespace-only faiss package directories: {removed}", "WARNING") except Exception as e: self.log(f"FAISS namespace cleanup failed: {e}", "WARNING") def check_package_installed(self, package_spec): """Check if a package meets the version requirement""" try: # Parse package specification (e.g., "transformers>=4.46.3") import re match = re.match(r'^([a-zA-Z0-9\-_]+)([><=!]+)?(.+)?$', package_spec) if not match: return False package_name = match.group(1) operator = match.group(2) if match.group(2) else None required_version = match.group(3) if match.group(3) else None # Use modern importlib.metadata (Python 3.8+) with fallback try: from importlib.metadata import version, PackageNotFoundError except ImportError: # Fallback for Python < 3.8 try: from importlib_metadata import version, PackageNotFoundError except ImportError: # Final fallback to pkg_resources (with warning suppression) import warnings with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) import pkg_resources try: distribution = pkg_resources.get_distribution(package_name) installed_version = distribution.version if not operator or not required_version: return True requirement = pkg_resources.Requirement.parse(package_spec) return distribution in requirement except pkg_resources.DistributionNotFound: return False try: installed_version = version(package_name) if not operator or not required_version: return True # Check version requirement using packaging module if available try: from packaging.specifiers import SpecifierSet from packaging.version import Version spec = SpecifierSet(f"{operator}{required_version}") return Version(installed_version) in spec except ImportError: # Simple version comparison fallback if operator == ">=": return installed_version >= required_version elif operator == ">": return installed_version > required_version elif operator == "==": return installed_version == required_version elif operator == "<=": return installed_version <= required_version elif operator == "<": return installed_version < required_version return True except PackageNotFoundError: return False except Exception: return False def has_onnxruntime_provider(self): """Return True when either CPU or GPU ONNX Runtime is already installed.""" return ( self.check_package_installed("onnxruntime-gpu>=1.19.0") or self.check_package_installed("onnxruntime>=1.17.0") ) def install_macos_specific_packages(self): """Install packages with Mac-specific requirements""" if not self.is_macos: return self.log("Installing macOS-specific audio packages", "INFO") # For M1 Macs, ensure we use compatible versions if self.is_m1_mac: self.log("M1 Mac detected - installing ARM64-compatible packages", "INFO") # Force reinstall samplerate with proper architecture self.run_pip_command( ["uninstall", "-y", "samplerate"], "Removing potentially x86_64 samplerate package", ignore_errors=True ) # Install with --no-cache to force ARM64 build self.run_pip_command( ["install", "--no-cache-dir", "--force-reinstall", "samplerate>=0.2.1"], "Installing ARM64-compatible samplerate package" ) # Install/reinstall audio packages that commonly have architecture issues on Mac mac_audio_packages = [ "soundfile>=0.12.0", "sounddevice>=0.4.0", "pydub", "webdataset", ] for package in mac_audio_packages: self.run_pip_command( ["install", "--force-reinstall", "--no-cache-dir", package], f"Reinstalling {package} for macOS compatibility" ) def install_core_dependencies(self): """Install safe core dependencies that don't cause conflicts""" self.log("Checking and installing core dependencies (with smart checking)", "INFO") core_packages = [ # Audio and basic utilities (PyTorch installed separately with CUDA) "soundfile>=0.12.0", "sounddevice>=0.4.0", # Text processing (safe) "jieba", "pypinyin", "unidecode", "omegaconf>=2.3.0", "transformers>=5.3.0,<6.0.0", # Main environment for Higgs Audio v3 / Step Audio EditX / MOSS-TTS. Legacy T4 engines use isolated runtimes. # Bundled engine dependencies (safe) "conformer>=0.3.2", # ChatterBox engine "x-transformers", # F5-TTS engine "torchdiffeq", # F5-TTS differential equations "wandb", # F5-TTS logging "accelerate", # F5-TTS acceleration "ema-pytorch", # F5-TTS exponential moving average "datasets", # F5-TTS dataset loading "vocos", # F5-TTS vocoder # Basic utilities (safe) "requests", "dacite", "safetensors>=0.6.2", # Required by MOSS-TTS HF checkpoints "orjson>=3.11.0", # Required by MOSS-TTS remote code "tiktoken>=0.12.0", # Required by MOSS-TTS tokenizer "fugashi>=1.4.0", # IndexTTS-2.5 Japanese G2P "unidic-lite>=1.0.8", # IndexTTS-2.5 Japanese dictionary # NOTE: opencv-python and pillow installed via install_problematic_packages() with --no-deps # to prevent forced numpy/pillow downgrades # SAFE packages from DEPENDENCY_TESTING_RESULTS.md "s3tokenizer>=0.1.7", # SAFE - Heavy dependencies but NO conflicts "vector-quantize-pytorch", # SAFE - Clean install "resemble-perth", # SAFE - Works in ChatterBox "diffusers>=0.30.0", # SAFE - Likely safe # "audio-separator>=0.35.2", # MOVED - Requires numpy>=2, installed conditionally "hydra-core>=1.3.0", # SAFE - Clean install, minimal dependencies # Dependencies for --no-deps packages based on PyPI metadata # For librosa (when installed with --no-deps) "lazy_loader>=0.1", # Required by librosa "msgpack>=1.0", # Required by librosa "pooch>=1.1", # Required by librosa "soxr>=0.3.2", # Required by librosa "typing_extensions>=4.1.1", # Required by librosa "decorator>=4.3.0", # Required by librosa "joblib>=1.0", # Required by librosa # For VibeVoice (when installed with --no-deps) - only safe dependencies "ml-collections", # Required by VibeVoice "absl-py", # Required by VibeVoice (Google's Python utilities) # NOTE: gradio installed via install_problematic_packages() with --no-deps # to prevent forced pydantic/pydantic-core downgrades "av", # Required by VibeVoice (PyAV - audio/video processing) "scikit-learn>=1.1.0", # Required by librosa # For cached-path (when installed with --no-deps) "filelock>=3.4", # Required by cached-path "rich>=12.1", # Required by cached-path "boto3", # Required by cached-path "google-cloud-storage", # Required by cached-path for F5-TTS "huggingface-hub", # Required by cached-path # For descript-audio-codec (when installed with --no-deps) "einops", # Required by descript-audio-codec and MelBandRoFormer "argbind>=0.3.7", # Required by descript-audio-codec # NOTE: descript-audiotools causes protobuf conflicts, installed via --no-deps # For MelBandRoFormer vocal separation "rotary_embedding_torch", # Required by MelBandRoFormer architecture # For F5-TTS engine "matplotlib", # Required by F5-TTS utils_infer.py # Additional librosa dependencies for --no-deps installation "audioread>=2.1.9", # Required by librosa "threadpoolctl>=3.1.0", # Required by scikit-learn for librosa # Missing descript-audiotools dependencies for --no-deps installation "flatten-dict", # Required by descript-audiotools "ffmpy", # Required by descript-audiotools "importlib-resources", # Required by descript-audiotools "randomname", # Required by descript-audiotools "markdown2", # Required by descript-audiotools "pyloudnorm", # Required by descript-audiotools "pystoi", # Required by descript-audiotools "torch-stoi", # Required by descript-audiotools "ipython", # Required by descript-audiotools "tensorboard", # Required by descript-audiotools "julius", # Required by descript-audiotools for DSP operations # IndexTTS-2 engine dependencies (tested safe - no conflicts found) "cn2an>=0.5.22", # Chinese number to Arabic number conversion "g2p-en>=2.1.0", # English grapheme-to-phoneme conversion "json5>=0.12.0", # JSON5 parsing for IndexTTS-2 config files "keras>=2.9.0", # Deep learning framework "modelscope>=1.27.0", # Chinese model hub for IndexTTS-2 "munch>=4.0.0", # Dictionary access with dot notation "sentencepiece>=0.2.1", # Text tokenization "textstat>=0.7.10", # Text statistics and readability ] # Smart installation: check before installing (preserving all original packages and comments) packages_to_install = [] skipped_packages = [] for package in core_packages: if self.check_package_installed(package): package_name = package.split('>=')[0].split('==')[0].split('<')[0] skipped_packages.append(package_name) else: packages_to_install.append(package) if skipped_packages: self.log(f"Already satisfied: {', '.join(skipped_packages[:5])}" + (f" and {len(skipped_packages)-5} others" if len(skipped_packages) > 5 else ""), "SUCCESS") if packages_to_install: self.log(f"Installing {len(packages_to_install)} missing core packages", "INFO") for package in packages_to_install: self.run_pip_command(["install", package], f"Installing {package}") else: self.log("All core dependencies already satisfied", "SUCCESS") def install_rvc_dependencies(self): """Install RVC voice conversion dependencies with smart GPU detection""" self.log("Installing RVC voice conversion dependencies", "INFO") # Install core RVC dependency first (with graceful failure for build-tool-less systems) if self.check_package_installed("monotonic-alignment-search"): self.log("monotonic-alignment-search already satisfied - skipping", "SUCCESS") else: self.run_pip_command(["install", "monotonic-alignment-search"], "Installing monotonic-alignment-search", ignore_errors=True) # RVC index building only uses the CPU FAISS Python API. # If users already have a working FAISS module (including a valid GPU build), keep it. if self.verify_faiss_installation(): self.log("Working FAISS API already available - skipping FAISS install", "SUCCESS") return self.log( "Installing faiss-cpu for RVC voice matching (GPU FAISS is not required for current RVC index building)", "INFO", ) # Broken Arch/Linux installs have shown namespace-only 'faiss' stubs and half-installed GPU wheels. self.run_pip_command( ["uninstall", "-y", "faiss", "faiss-cpu", "faiss-gpu", "faiss-gpu-cu11", "faiss-gpu-cu12"], "Removing broken or conflicting FAISS packages", ignore_errors=True, ) self.cleanup_stale_faiss_namespace() self.run_pip_command( ["install", "--force-reinstall", "--no-cache-dir", "--no-deps", "faiss-cpu>=1.7.4"], "Installing faiss-cpu for RVC voice matching (--no-deps)", ) if not self.verify_faiss_installation(): self.log( "faiss-cpu installation completed but working FAISS APIs are still unavailable. " "This usually means a stale site-packages/faiss directory or a broken wheel in the environment.", "WARNING", ) def install_numpy_with_constraints(self): """Install numpy with version constraints for compatibility""" self.log("Checking numpy compatibility", "INFO") # Python 3.13+ requires numpy 2.1.0 or newer (no wheels for 1.26.x) if self.python_version >= (3, 13): minimum_numpy = "numpy>=2.1.0,<2.3.0" self.log("Python 3.13+ detected - requires NumPy 2.1.0 or newer", "INFO") else: minimum_numpy = "numpy>=1.26.4,<2.3.0" # Check if numpy is already installed and what version try: import numpy numpy_version = numpy.__version__ self.log(f"Current numpy version: {numpy_version}", "INFO") # Parse numpy version import re version_match = re.match(r'(\d+)\.(\d+)', numpy_version) if version_match: major, minor = int(version_match.group(1)), int(version_match.group(2)) # Python 3.13+ specific check if self.python_version >= (3, 13) and major < 2: self.log(f"NumPy {numpy_version} is incompatible with Python 3.13+", "ERROR") self.log("Python 3.13 requires NumPy 2.1.0 or newer (no wheels for 1.26.x)", "WARNING") numpy_constraint = minimum_numpy # Accept both numpy 1.26.x and 2.x.x (but not 2.3.x) for older Python elif major == 1 and minor >= 26 and self.python_version < (3, 13): self.log(f"NumPy {numpy_version} is compatible - keeping current version", "INFO") return # NumPy 1.26.x is fine for Python < 3.13 elif major == 2 and minor < 3: self.log(f"NumPy {numpy_version} is compatible - keeping current version", "INFO") return # NumPy 2.0.x, 2.1.x, 2.2.x are fine elif major >= 2 and minor >= 3: # Only numpy 2.3+ needs downgrading self.log(f"NumPy {numpy_version} may cause issues - constraining to <2.3.0", "WARNING") numpy_constraint = minimum_numpy else: # Very old numpy needs updating self.log(f"NumPy {numpy_version} is too old", "WARNING") numpy_constraint = minimum_numpy else: # Can't parse version - install safe range numpy_constraint = minimum_numpy except ImportError: # No numpy installed - install with appropriate constraints self.log("NumPy not found - installing with appropriate constraints", "INFO") numpy_constraint = minimum_numpy except Exception as e: # NumPy import failed self.log(f"NumPy check failed ({e}) - installing safe version", "WARNING") numpy_constraint = minimum_numpy # Only install/upgrade numpy if needed if 'numpy_constraint' in locals(): self.run_pip_command(["install", numpy_constraint], "Installing numpy with version constraints") # Note: We're not forcing numba installation anymore since we disable JIT anyway self.log("NumPy compatibility check complete", "INFO") def install_audio_separator_if_compatible(self): """Install audio-separator only if numpy version supports it""" try: import numpy numpy_version = numpy.__version__ # Parse numpy version import re version_match = re.match(r'(\d+)\.(\d+)', numpy_version) if version_match: major, minor = int(version_match.group(1)), int(version_match.group(2)) if major >= 2: # NumPy 2.x can use audio-separator if self.check_package_installed("audio-separator>=0.35.2"): self.log("audio-separator already satisfied - skipping", "SUCCESS") else: self.log(f"NumPy {numpy_version} supports audio-separator - installing", "INFO") self.run_pip_command( ["install", "audio-separator>=0.35.2"], "Installing audio-separator for enhanced vocal removal", ignore_errors=True # It's optional, so don't fail if it doesn't install ) else: # NumPy 1.x - skip audio-separator, will use bundled implementations self.log(f"NumPy {numpy_version} detected - skipping audio-separator (will use bundled vocal removal)", "INFO") self.log("Vocal removal will use bundled RVC/MelBand/MDX23C implementations", "INFO") else: # Can't determine version - skip to be safe self.log("Could not determine NumPy version - skipping audio-separator", "WARNING") except ImportError: # NumPy not installed? This shouldn't happen at this point self.log("NumPy not found - skipping audio-separator installation", "WARNING") except Exception as e: # Any other error - skip audio-separator self.log(f"Error checking NumPy compatibility for audio-separator: {e}", "WARNING") self.log("Skipping audio-separator - vocal removal will use bundled implementations", "INFO") def install_gradio_and_opencv_dependencies(self): """Pre-install safe dependencies for gradio and opencv-python""" self.log("Pre-installing safe dependencies for gradio and opencv-python", "INFO") # Only pre-install packages that won't be downgraded # pydantic and pydantic-core MUST be installed by gradio itself (they have version requirements) # pillow is safe to pre-install with flexible version range gradio_safe_deps = [ "fastapi<1.0,>=0.115.2", # FastAPI for gradio server "starlette<0.50.0,>=0.40.0", # Web framework (starlette 0.50.x+) "gradio-client==1.13.3", # Specific version for gradio compatibility "websockets<16.0,>=13.0", # WebSocket support "python-multipart>=0.0.18", # Form parsing "uvicorn>=0.14.0", # ASGI server "annotated-doc>=0.0.2", # FastAPI dependency ] # Install safe dependencies - these won't cause downgrades for dep in gradio_safe_deps: if self.check_package_installed(dep): self.log(f"Pre-installing: {dep} (already satisfied)", "SUCCESS") continue self.run_pip_command( ["install", dep], f"Pre-installing: {dep}", ignore_errors=True ) # NOTE: pydantic and pydantic-core are NOT pre-installed here # Gradio requires specific versions (pydantic<2.12, pydantic-core==2.33.2) # These will be installed when gradio installs with --no-deps # This is the trade-off: some packages will be downgraded, but only to versions # that are compatible with the software that needs them def install_onnxruntime_with_gpu_support(self): """Install ONNX Runtime with GPU acceleration if CUDA is available""" self.log("Installing ONNX Runtime (OpenSeeFace, Step Audio EditX)", "INFO") if self.has_onnxruntime_provider(): if self.check_package_installed("onnxruntime-gpu>=1.19.0"): self.log("Existing onnxruntime-gpu detected - keeping GPU runtime", "SUCCESS") else: self.log("Existing onnxruntime detected - skipping install", "SUCCESS") return cuda_version = self.detect_cuda_version() # Try GPU version first if CUDA is available if cuda_version != "cpu": if self.check_package_installed("onnxruntime-gpu>=1.19.0"): self.log("onnxruntime-gpu already satisfied - skipping", "SUCCESS") return self.log("CUDA detected - attempting onnxruntime-gpu for GPU acceleration", "INFO") try: # Try GPU version without --no-deps (modern versions don't force numpy downgrade) # Relaxed constraint to >=1.19.0 to avoid forcing upgrades from 1.22.0 gpu_success = self.run_pip_command( ["install", "onnxruntime-gpu>=1.19.0"], "Installing onnxruntime-gpu (GPU acceleration for ONNX models)", ignore_errors=True ) if gpu_success: # Trust that onnxruntime-gpu installation succeeded # Note: CUDA provider verification can fail in restricted environments (Docker, containers) # even when onnxruntime-gpu works fine at runtime. Installing both CPU and GPU versions # causes Python to prefer the CPU version, degrading performance. self.log("onnxruntime-gpu installed successfully", "SUCCESS") return except subprocess.CalledProcessError: self.log("onnxruntime-gpu installation failed - falling back to CPU version", "WARNING") # Fallback to CPU version if self.check_package_installed("onnxruntime>=1.17.0"): self.log("onnxruntime already satisfied - skipping", "SUCCESS") else: self.log("Installing onnxruntime (CPU) for OpenSeeFace and Step Audio EditX", "INFO") self.run_pip_command( ["install", "onnxruntime>=1.17.0"], "Installing onnxruntime (CPU version)", ignore_errors=True ) def install_problematic_packages(self): """Install packages that cause conflicts using --no-deps""" self.log("Installing problematic packages with --no-deps to prevent conflicts", "WARNING") problematic_packages = [ "librosa", # Forces numpy downgrade - compatibility handled by runtime numba disabling for Python 3.13 "descript-audio-codec", # Pulls unnecessary deps, conflicts with protobuf "descript-audiotools", # Forces protobuf downgrade from 6.x to 3.19.x "cached-path", # Forces package downgrades "omnivoice", # Keep shared dependency resolution under our control "torchcrepe", # Conflicts via librosa dependency # NOTE: onnxruntime moved to install_onnxruntime_with_gpu_support() for smart GPU detection "opencv-python", # Forces numpy downgrade from 2.x to 1.26.x - dependencies pre-installed "gradio", # Forces pydantic, pillow, pydantic-core downgrades - dependencies pre-installed ] for package in problematic_packages: if self.check_package_installed(package): self.log(f"{package} already satisfied - skipping", "SUCCESS") continue self.run_pip_command( ["install", package, "--no-deps"], f"Installing {package} (--no-deps)", ignore_errors=True # Some may already be satisfied ) def install_vibevoice(self): """Install VibeVoice with careful dependency management""" self.log("Installing VibeVoice TTS engine", "INFO") # First ensure critical dependencies that VibeVoice needs but might downgrade # Handle 'av' (PyAV) dependency specifically - frequent failure point on Windows self.log("Checking VibeVoice 'av' dependency...", "INFO") if self.check_package_installed("av"): av_success = True self.log("av already satisfied - skipping", "SUCCESS") else: # Try installing av first (often fails on Windows without Build Tools) av_success = self.run_pip_command(["install", "av"], "Installing av (PyAV)", ignore_errors=True) if not av_success: self.log("Failed to install 'av' (PyAV) - VibeVoice may not work", "WARNING") if self.is_windows: self.log("Windows Fix: Install Visual C++ Build Tools or use a compatible Python version", "INFO") self.log("This is often due to missing pre-built wheels for your Python version", "INFO") # Now install VibeVoice with --no-deps to prevent downgrades # NOTE: Using FushionHub fork temporarily - Microsoft removed the official repo # Original: https://github.com/microsoft/VibeVoice.git (no longer exists) # This fork maintains the same API and should work identically self.log("Installing VibeVoice with --no-deps to prevent package downgrades", "WARNING") if self.check_package_installed("vibevoice"): self.log("VibeVoice already satisfied - skipping", "SUCCESS") else: self.run_pip_command( ["install", "git+https://github.com/FushionHub/VibeVoice.git", "--no-deps"], "Installing VibeVoice (--no-deps)", ignore_errors=True ) def install_echo_tts(self): """Install Echo-TTS with minimal dependency impact""" self.log("Installing Echo-TTS engine", "INFO") if self.check_package_installed("echo-tts"): self.log("echo-tts already satisfied - skipping", "SUCCESS") return installed = self.run_pip_command( ["install", "echo-tts", "--no-deps"], "Installing Echo-TTS (--no-deps)", ignore_errors=True ) if not installed: self.log("Echo-TTS pip install failed - trying GitHub source", "WARNING") self.run_pip_command( ["install", "git+https://github.com/jordandare/echo-tts.git", "--no-deps"], "Installing Echo-TTS from GitHub (--no-deps)", ignore_errors=True ) def install_dots_tts(self): """Install official Dots TTS with minimal dependency impact.""" self.log("Installing Dots TTS engine", "INFO") dependency_probes = [ ("lingua", ["install", "lingua-language-detector"], "Installing lingua-language-detector"), ("langcodes", ["install", "langcodes"], "Installing langcodes"), ("loguru", ["install", "loguru"], "Installing loguru"), ("tn.chinese.normalizer", ["install", "WeTextProcessing"], "Installing WeTextProcessing"), ] for module_name, pip_args, description in dependency_probes: if self.module_available(module_name): self.log(f"{module_name} import already satisfied - skipping", "SUCCESS") continue if module_name == "tn.chinese.normalizer" and self.is_windows: self.log( "WeTextProcessing/tn is unavailable on Windows; skipping the known pynini source build", "WARNING", ) continue self.run_pip_command(pip_args, description, ignore_errors=True) if self.module_available("dots_tts.runtime") and self.module_available("tn.chinese.normalizer"): self.log("dots_tts.runtime already satisfied - skipping", "SUCCESS") return if self.check_package_installed("dots.tts") and self.module_available("dots_tts.runtime"): self.log( "Dots package is installed but its tn/WeTextProcessing prerequisite is unavailable; skipping rebuild", "WARNING", ) return dots_install_args = [ "install", "git+https://github.com/rednote-hilab/dots.tts.git", "--no-deps", ] if self.is_python_313: # Dots currently declares <3.13, although its source works in the # suite's tested Python 3.13 environment. Keep the existing stack # and bypass only pip's metadata gate. dots_install_args.insert(1, "--ignore-requires-python") self.log( "Python 3.13 detected - allowing the tested Dots source install despite its package metadata", "WARNING", ) self.run_pip_command( dots_install_args, "Installing official dots.tts from GitHub (--no-deps)", ignore_errors=True ) def install_fish_audio_s2(self): """Install Fish S2 without changing the suite's core ML stack.""" self.log("Installing Fish Audio S2 support in the main T5 environment", "INFO") # The adapter uses Fish's official inference_engine API. Pin the tested # S2 release because distribution metadata alone cannot verify that API. fish_repository = "https://github.com/fishaudio/fish-speech.git" fish_revision = "v2.0.0-beta" fish_runtime_module = "fish_speech.inference_engine" packages = [ ("lightning", "lightning"), ("pytorch_lightning", "pytorch-lightning"), ("lightning_utilities", "lightning-utilities"), ("torchmetrics", "torchmetrics"), ("opencc", "opencc-python-reimplemented==0.1.7"), ("ormsgpack", "ormsgpack"), ("zstandard", "zstandard"), ("pyrootutils", "pyrootutils"), ("loralib", "loralib"), ] for module_name, package_name in packages: if self.module_available(module_name): self.log(f"{module_name} import already satisfied - skipping", "SUCCESS") continue self.run_pip_command( ["install", package_name, "--no-deps"], f"Installing Fish S2 dependency {package_name} (--no-deps)", ignore_errors=True, ) if self.module_available(fish_runtime_module): self.log("Fish S2 inference runtime already satisfied - skipping", "SUCCESS") return if self.check_package_installed("fish-speech"): self.log( "fish-speech distribution is installed but the required Fish S2 inference runtime is missing - repairing", "WARNING", ) # The upstream beta pyproject packages only fish_speech itself, so its # wheel omits fish_speech.inference_engine. Install the wheel for its # metadata, then restore the complete official source package. with tempfile.TemporaryDirectory(prefix="tts_fish_s2_") as temp_dir: source_dir = Path(temp_dir) / "fish-speech" env = os.environ.copy() if self.is_windows: env["PYTHONUTF8"] = "1" env["PYTHONIOENCODING"] = "utf-8" self.log("Downloading tested Fish S2 source revision...", "INSTALL") try: clone_result = subprocess.run( [ "git", "clone", "--depth", "1", "--branch", fish_revision, fish_repository, str(source_dir), ], capture_output=True, text=True, encoding="utf-8" if self.is_windows else None, errors="replace" if self.is_windows else None, check=True, env=env, ) if clone_result.stdout.strip(): print(clone_result.stdout) except (OSError, subprocess.CalledProcessError) as error: details = getattr(error, "stderr", None) or str(error) self.log(f"Fish S2 source download failed: {details.strip()}", "ERROR") else: installed = self.run_pip_command( ["install", "--upgrade", "--force-reinstall", str(source_dir), "--no-deps"], "Installing tested Fish S2 runtime from GitHub (--no-deps)", ignore_errors=True, ) if installed: self._restore_fish_source_package(source_dir) if self.module_available(fish_runtime_module): self.log("Fish S2 inference runtime installed successfully", "SUCCESS") else: self.log( "Fish S2 inference runtime is still unavailable after installation; Fish Audio S2 will not work until the package is repaired", "ERROR", ) def _restore_fish_source_package(self, source_dir: Path) -> bool: """Restore Fish subpackages omitted by the upstream wheel metadata.""" source_package = source_dir / "fish_speech" if not source_package.is_dir(): self.log("Fish S2 source checkout does not contain fish_speech", "ERROR") return False import site import sysconfig package_roots = list(sys.path) package_roots.extend(site.getsitepackages()) package_roots.append(site.getusersitepackages()) package_roots.extend( path for path in ( sysconfig.get_paths().get("purelib"), sysconfig.get_paths().get("platlib"), ) if path ) target_package = None seen_roots = set() for entry in package_roots: if not entry or entry in seen_roots: continue seen_roots.add(entry) candidate = Path(entry) / "fish_speech" # Fish S2 is an implicit namespace package and intentionally has # no fish_speech/__init__.py in the official source tree. if candidate.is_dir(): target_package = candidate break if target_package is None: self.log("Could not locate the active site-packages/fish_speech directory", "ERROR") return False try: shutil.copytree(source_package, target_package, dirs_exist_ok=True) except OSError as error: self.log(f"Could not restore the Fish S2 source package: {error}", "ERROR") return False importlib.invalidate_caches() self.log( f"Restored complete Fish S2 source package in {target_package}", "SUCCESS", ) return True def install_f5tts_multilingual_support(self): """Install phonemization support for F5-TTS multilingual models (Polish, German, French, Spanish, etc.)""" self.log("Installing F5-TTS multilingual phonemization support", "INFO") if self.is_windows: # Windows: pip package that includes espeak binaries (no separate system install needed) self.log("Windows detected - installing espeak-phonemizer-windows for multilingual F5-TTS", "INFO") if self.check_package_installed("espeak-phonemizer-windows"): self.log("espeak-phonemizer-windows already satisfied - skipping", "SUCCESS") else: self.run_pip_command( ["install", "espeak-phonemizer-windows"], "Installing espeak-phonemizer-windows (includes binaries)", ignore_errors=True ) # Test if it works try: import subprocess result = subprocess.run([ sys.executable, "-c", "from espeak_phonemizer import Phonemizer; p=Phonemizer(); p.phonemize('test', voice='en')" ], capture_output=True, timeout=10) if result.returncode == 0: self.log("espeak-phonemizer-windows working - multilingual F5-TTS models will work properly", "SUCCESS") else: self.log("espeak-phonemizer-windows test failed - non-English F5-TTS models may have quality issues", "WARNING") except Exception: self.log("Could not test espeak-phonemizer-windows installation", "WARNING") else: # Linux/Mac: pip package + separate system dependency self.log("Linux/Mac detected - installing phonemizer for multilingual F5-TTS support", "INFO") if self.check_package_installed("phonemizer"): phonemizer_installed = True self.log("phonemizer already satisfied - skipping", "SUCCESS") else: phonemizer_installed = self.run_pip_command( ["install", "phonemizer"], "Installing phonemizer package", ignore_errors=True ) if phonemizer_installed: # Test if system espeak is available try: import subprocess result = subprocess.run([ sys.executable, "-c", "from phonemizer import phonemize; phonemize('test', language='en', backend='espeak')" ], capture_output=True, timeout=10) if result.returncode == 0: self.log("phonemizer + system espeak working - multilingual F5-TTS models will work properly", "SUCCESS") else: self.log("phonemizer installed but system espeak dependency missing", "WARNING") self.log("To enable multilingual F5-TTS support, install system espeak:", "INFO") if self.is_macos: print(" brew install espeak") else: # Linux print(" sudo apt-get install espeak espeak-data # Ubuntu/Debian") print(" sudo dnf install espeak espeak-devel # Fedora/RHEL") self.log("Non-English F5-TTS models will fall back to character-based processing", "WARNING") except Exception as e: self.log(f"Could not test phonemizer installation: {e}", "WARNING") else: self.log("phonemizer installation failed - non-English F5-TTS models will use fallback processing", "WARNING") def install_indexts_text_processing(self): """Install IndexTTS-2 text processing with smart fallback handling""" self.log("Installing IndexTTS-2 text normalization support", "INFO") # WeTextProcessing requires manual wheel installation on Windows (build tools needed) # Skip it on Windows and use fallback instead to avoid confusing error messages if self.is_windows: self.log("Windows detected - WeTextProcessing requires manual build, using fallback approach", "INFO") wetextprocessing_success = False else: # Try WeTextProcessing first (newer, preferred package) - only on Linux/Mac wetextprocessing_success = self.run_pip_command( ["install", "WeTextProcessing"], "Installing WeTextProcessing (Chinese/English normalization)", ignore_errors=True ) if wetextprocessing_success: # Test if WeTextProcessing actually works try: result = subprocess.run([ sys.executable, "-c", "from WeTextProcessing import Normalizer; n=Normalizer(lang='en', operator='tn'); print('OK')" ], capture_output=True, timeout=10) if result.returncode == 0: self.log("WeTextProcessing installed and working - IndexTTS-2 will have full text normalization", "SUCCESS") return else: self.log("WeTextProcessing installed but not working - trying fallback", "WARNING") except Exception: self.log("Could not test WeTextProcessing - trying fallback", "WARNING") # Fallback to wetext (older package, more compatible) if self.check_package_installed("wetext"): wetext_success = True self.log("wetext already satisfied - skipping", "SUCCESS") else: wetext_success = self.run_pip_command( ["install", "wetext"], "Installing wetext (fallback text normalization)", ignore_errors=True ) if wetext_success: try: result = subprocess.run([ sys.executable, "-c", "from wetext import Normalizer; n=Normalizer(lang='en', operator='tn'); print('OK')" ], capture_output=True, timeout=10) if result.returncode == 0: self.log("wetext fallback working - IndexTTS-2 will have basic text normalization", "SUCCESS") return else: self.log("wetext installed but not working", "WARNING") except Exception: self.log("Could not test wetext installation", "WARNING") # Both failed - IndexTTS-2 will use basic processing self.log("Text normalization packages failed to install - IndexTTS-2 will use basic text processing", "WARNING") self.log("This may affect quality for Chinese text and complex English patterns", "INFO") def install_russian_text_stresser_support(self): """Install lightweight Russian stress support for Official 23-Lang.""" self.log("Installing Russian stress labeling support for Official 23-Lang", "INFO") dependency_specs = [ "poetry-core>=2.0.0", "spacy<4", "pymorphy2>=0.9.1", "stressed-cyrillic-tools>=0.1.10", ] for spec in dependency_specs: if self.check_package_installed(spec): self.log(f"{spec} already satisfied - skipping", "SUCCESS") continue self.run_pip_command(["install", spec], f"Installing {spec}", ignore_errors=True) if self.check_package_installed("russian-text-stresser>=0.1.2"): self.log("Patched russian-text-stresser already satisfied - skipping", "SUCCESS") else: installed = self.run_pip_command( [ "install", "--force-reinstall", "--no-deps", "--no-build-isolation", "--ignore-requires-python", self.russian_stress_fork_ref, ], "Installing patched russian-text-stresser from fork", ignore_errors=True, ) if not installed: self.log("Patched russian-text-stresser installation failed - Russian stress marks will remain unavailable", "WARNING") return env = os.environ.copy() if self.is_windows: env["PYTHONUTF8"] = "1" env["PYTHONIOENCODING"] = "utf-8" try: result = subprocess.run( [ sys.executable, "-c", ( "from russian_text_stresser import russian_dictionary as rd; " "print('OK' if hasattr(rd, 'DATA_DIR_ENV_VAR') else 'MISSING_PATCH')" ), ], capture_output=True, text=True, timeout=30, env=env, encoding="utf-8" if self.is_windows else None, ) if result.returncode == 0 and "OK" in (result.stdout or ""): self.log("Russian stress labeling package installed", "SUCCESS") else: error_text = (result.stderr or result.stdout or "").strip() self.log(f"Russian stress labeling package validation failed: {error_text}", "WARNING") except Exception as e: self.log(f"Could not validate russian-text-stresser installation: {e}", "WARNING") def check_python_environment(self): """Check Python environment and warn about potential mismatches""" python_path = sys.executable.lower() # Check for virtual environment mismatch (Windows py launcher issue) if 'VIRTUAL_ENV' in os.environ: venv_path = os.environ['VIRTUAL_ENV'] if self.is_windows and not sys.executable.startswith(venv_path): self.log("WARNING: Python version mismatch detected!", "WARNING") self.log(f"Virtual environment: {venv_path}", "WARNING") self.log(f"Current Python: {sys.executable}", "WARNING") self.log("You likely used 'py install.py' instead of 'python install.py'", "WARNING") self.log("Use 'python install.py' to match your ComfyUI Python version", "INFO") print() # Add spacing for visibility return False # Check for clearly identifiable system Python paths system_python_patterns = [ "c:\\python", # Windows system Python "/usr/bin/python", # Linux system Python "/usr/local/bin/python", # macOS Homebrew system Python "system32", # Windows system directory ] if any(pattern in python_path for pattern in system_python_patterns): self.log("WARNING: Detected system-wide Python installation", "WARNING") self.log(f"Current Python: {sys.executable}", "WARNING") self.log("This may install packages to the wrong location", "WARNING") self.log("For best results, use ComfyUI Manager for automatic installation", "INFO") return False return True def handle_wandb_issues(self): """Fix wandb circular import issues that affect multiple nodes""" self.log("Checking and fixing wandb import issues", "INFO") try: # Try to import wandb to check for issues import wandb # Try to access the errors attribute that's causing the circular import hasattr(wandb, 'errors') self.log("wandb import test passed", "SUCCESS") except (ImportError, AttributeError) as e: self.log(f"wandb import issue detected: {e}", "WARNING") self.log("Reinstalling wandb to fix circular import", "WARNING") # Uninstall and reinstall wandb cleanly self.run_pip_command( ["uninstall", "-y", "wandb"], "Uninstalling problematic wandb", ignore_errors=True ) # Clear any cached/partial installations self.run_pip_command( ["install", "--no-cache-dir", "--force-reinstall", "wandb>=0.17.0"], "Reinstalling wandb cleanly" ) # Test again after reinstallation try: import importlib importlib.invalidate_caches() # Clear import cache import wandb hasattr(wandb, 'errors') self.log("wandb reinstallation successful", "SUCCESS") except Exception as retry_error: self.log(f"wandb reinstallation still has issues: {retry_error}", "ERROR") self.log("Some F5-TTS and Higgs features may not work properly", "WARNING") def handle_python_313_specific(self): """Handle Python 3.13 specific compatibility issues""" if not self.is_python_313: self.log("Python < 3.13 detected, skipping 3.13-specific workarounds", "INFO") return self.log("Python 3.13 detected - applying compatibility measures", "WARNING") # MediaPipe is incompatible - inform user about OpenSeeFace alternative self.log("MediaPipe is incompatible with Python 3.13", "WARNING") self.log("OpenSeeFace will be used automatically for mouth movement analysis", "INFO") self.log("Note: OpenSeeFace is experimental and may be less accurate than MediaPipe", "WARNING") # OpenSeeFace only needs an ONNX Runtime provider. Do not replace a working GPU install. if self.has_onnxruntime_provider(): if self.check_package_installed("onnxruntime-gpu>=1.19.0"): self.log("Python 3.13: onnxruntime-gpu already available - skipping CPU reinstall", "SUCCESS") else: self.log("Python 3.13: onnxruntime already available - skipping reinstall", "SUCCESS") return self.run_pip_command( ["install", "onnxruntime", "--no-deps"], "Installing onnxruntime for OpenSeeFace (Python 3.13)", ignore_errors=True ) def validate_installation(self): """Validate package presence without importing heavyweight runtimes.""" self.log("Validating installation...", "INFO") validation_errors = [] for module_name, display_name in self.CORE_MODULE_CHECKS: if self.module_available(module_name): self.log(f"{display_name}: OK", "SUCCESS") else: validation_errors.append(f"{display_name}: module is unavailable") self.log(f"{display_name}: FAILED - module is unavailable", "ERROR") # Validate actual runtime entry points instead of relying on package # distribution metadata. This is intentionally presence-only. for display_name, module_names in self.ENGINE_RUNTIME_CHECKS: missing_modules = [ module_name for module_name in module_names if not self.module_available(module_name) ] available = not missing_modules self.engine_validation_results[display_name] = available if available: self.log(f"{display_name} runtime: OK", "SUCCESS") else: validation_errors.append( f"{display_name} runtime is unavailable ({', '.join(missing_modules)})" ) self.log( f"{display_name} runtime: FAILED ({', '.join(missing_modules)})", "ERROR", ) # Dots supplies a no-op normalizer when tn/WeTextProcessing is absent; # this affects text normalization quality, not engine availability. if not self.module_available("tn.chinese.normalizer"): warning = "Dots TTS text normalization unavailable; Dots will use its built-in no-op fallback" self.add_install_warning( "Dots TTS text normalization (optional)", warning, ) self.log(warning, "WARNING") # Check Python 3.13 specific validations if self.is_python_313: if self.module_available("onnxruntime"): self.log("ONNXRuntime (OpenSeeFace): OK", "SUCCESS") else: validation_errors.append("ONNXRuntime required for OpenSeeFace on Python 3.13") self.log("ONNXRuntime (OpenSeeFace): FAILED", "ERROR") # Check RVC dependencies rvc_modules = [("monotonic_alignment_search", "Monotonic Alignment Search")] for module_name, display_name in rvc_modules: if self.module_available(module_name): self.log(f"{display_name} (RVC): OK", "SUCCESS") else: # RVC is optional, so this is just a warning self.log(f"{display_name} (RVC): Not available - RVC voice conversion will not work", "WARNING") return len(validation_errors) == 0 def check_version_conflicts(self): """Check for known version conflicts""" self.log("Checking for version conflicts...", "INFO") try: from importlib.metadata import version, PackageNotFoundError numpy_version_text = version("numpy") numpy_version = tuple(map(int, numpy_version_text.split('.')[:2])) if numpy_version >= (2, 3): self.log(f"WARNING: NumPy {numpy_version_text} detected - may cause numba conflicts", "WARNING") self.log("Consider downgrading: pip install 'numpy>=2.2.0,<2.3.0'", "WARNING") else: self.log(f"NumPy {numpy_version_text}: Version OK for compatibility", "SUCCESS") except (PackageNotFoundError, ImportError, ValueError): self.log("NumPy not found - this will cause issues", "ERROR") def print_installation_summary(self, installation_valid: bool): """Print installation summary and next steps""" print("\n" + "="*70) print(" "*20 + "TTS AUDIO SUITE INSTALLATION") print("="*70) if installation_valid: if self.install_warnings: self.log( f"Installation completed with {len(self.install_warnings)} warning(s)", "WARNING", ) else: self.log("Installation completed successfully!", "SUCCESS") else: self.log("Installation completed with errors - one or more required runtimes are unavailable", "ERROR") print(f"\n>>> Python version: {self.python_version.major}.{self.python_version.minor}.{self.python_version.micro}") if self.is_macos: print(f">>> Platform: macOS ({platform.machine()})") if self.is_python_313: print("\n" + "-"*50) print(" PYTHON 3.13 COMPATIBILITY STATUS") print("-"*50) print(" [+] Python 3.13 compatibility workarounds applied") print(" [+] OpenSeeFace mouth movement: WORKING (experimental)") print(" [+] Numba/Librosa compatibility: FIXED") print(" -> Automatic JIT disabling for Python 3.13") print(" [X] MediaPipe mouth movement: INCOMPATIBLE") print(" -> Use OpenSeeFace alternative") print("\n>> Want MediaPipe Python 3.13 support? Vote at:") print(" https://github.com/google-ai-edge/mediapipe/issues/5708") else: print("\n" + "-"*50) print(" CORE COMPATIBILITY STATUS") print("-"*50) print(" [+] Core compatibility checks completed") print("\n" + "-"*50) print(" EXTERNAL ENGINE RUNTIME CHECKS") print("-"*50) print(" Bundled and isolated engines use the core/shared runtime checks.") for display_name, available in self.engine_validation_results.items(): status = "AVAILABLE" if available else "UNAVAILABLE" marker = "+" if available else "X" print(f" [{marker}] {display_name}: {status}") if self.install_warnings: print("\n" + "-"*50) print(" INSTALLATION WARNINGS") print("-"*50) for description, _ in self.install_warnings: print(f" [!] {description}") print("\n" + "="*70) if installation_valid: print(" "*15 + "READY TO USE TTS AUDIO SUITE IN COMFYUI!") else: print(" "*12 + "INSTALLATION NEEDS ATTENTION BEFORE USE") print("="*70) # Windows-specific note about text normalization if self.is_windows: print("\n" + "-"*50) print(" WINDOWS NOTES") print("-"*50) print(" WeTextProcessing was skipped (requires manual wheel build).") print(" IndexTTS-2 will use fallback text processing instead - this") print(" means slightly lower quality for Chinese text, but all TTS") print(" generation will work fine.") print("\n This is normal and expected on Windows!") print() def main(): """Main installation entry point""" installer = TTSAudioInstaller() try: installer.log("Starting TTS Audio Suite installation", "INFO") installer.log(f"Python {installer.python_version.major}.{installer.python_version.minor}.{installer.python_version.micro} detected", "INFO") # Check environment and system dependencies before proceeding installer.check_python_environment() if installer.can_skip_dependency_installation(): installer.log("No dependency changes detected; continuing with fast validation only", "INFO") else: installer.ensure_requirements_installed() # Ensure requirements.txt is installed first # Report optional system dependencies without blocking engine setup. installer.check_system_dependencies() # Install in correct order to prevent conflicts installer.install_pytorch_with_cuda() # Install PyTorch first with proper CUDA detection installer.install_core_dependencies() installer.install_macos_specific_packages() # Mac-specific package fixes installer.install_numpy_with_constraints() installer.install_audio_separator_if_compatible() # Install audio-separator only if numpy>=2 installer.install_rvc_dependencies() installer.install_gradio_and_opencv_dependencies() # Pre-install deps before --no-deps installer.install_problematic_packages() installer.install_onnxruntime_with_gpu_support() # Install ONNX with GPU acceleration if available installer.install_vibevoice() # Install VibeVoice with careful dependency management installer.install_echo_tts() # Install Echo-TTS with minimal dependency impact installer.install_dots_tts() # Install official Dots TTS in the main environment first installer.install_fish_audio_s2() # Install Fish S2 without changing Torch/Transformers installer.install_f5tts_multilingual_support() # Install phonemization for Polish/multilingual F5-TTS installer.install_indexts_text_processing() # Install IndexTTS-2 text normalization with fallback installer.install_russian_text_stresser_support() # Install lightweight Russian stress package for Official 23-Lang installer.handle_wandb_issues() # Fix wandb circular import installer.handle_python_313_specific() # Validation and summary installer.check_version_conflicts() success = installer.validate_installation() installer.save_installation_state(success) installer.print_installation_summary(success) if not success: installer.log("Installation finished with errors - review the failed runtime checks above", "ERROR") sys.exit(1) elif installer.install_warnings: installer.log("Installation finished with warnings - review the installation warnings above", "WARNING") sys.exit(0) else: installer.log("Installation completed successfully!", "SUCCESS") sys.exit(0) except Exception as e: installer.log(f"Installation failed: {e}", "ERROR") installer.log("Please check the error messages above and try again", "ERROR") sys.exit(1) if __name__ == "__main__": main()