Files
diodiogod-TTS-Audio-Suite/install.py
T
diodiogod caee9dbcd1 Version 4.11.23
Fix PyTorch installation syntax error

Invalid pip requirement syntax with local version labels in version specifiers.
The +cu121 suffix is only valid with == or != operators, not with >= operators.
Now correctly passes CUDA version via --index-url instead.

Also add graceful error handling for RVC monotonic-alignment-search installation
on systems without C++ build tools.
2025-10-26 19:58:14 -03:00

1203 lines
58 KiB
Python

#!/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.
"""
import subprocess
import sys
import os
import platform
from typing import List, Optional
class TTSAudioInstaller:
"""Intelligent installer for TTS Audio Suite with Python 3.13 compatibility"""
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"]
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 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 names
missing_packages = []
try:
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
line = line.split('#')[0].strip()
if not line:
continue
# Extract package name (before >= <= == etc.)
package_name = line.split('>=')[0].split('<=')[0].split('==')[0].split('<')[0].split('>')[0].split('!')[0].strip()
if package_name:
try:
# Handle package name differences (dashes vs underscores)
import_name = package_name.replace("-", "_")
__import__(import_name)
except ImportError:
missing_packages.append(package_name)
except Exception as e:
self.log(f"Error reading requirements.txt: {e}", "WARNING")
return
if missing_packages:
self.log(f"Missing {len(missing_packages)} requirements.txt packages: {', '.join(missing_packages[:5])}{'...' if len(missing_packages) > 5 else ''}", "WARNING")
self.log("Installing missing requirements individually (preserves ComfyUI Manager safeguards)", "INFO")
# Install each missing package individually using our safe method
for package in missing_packages:
# Get the full package spec from requirements.txt
package_spec = package
try:
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 clean_line and clean_line.startswith(package):
package_spec = clean_line
break
except:
pass
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 for required system libraries and provide helpful error messages"""
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 check_macos_dependencies(self):
"""Check for required system libraries on macOS"""
self.log("Checking macOS system dependencies...", "INFO")
missing_deps = []
# Check for libsamplerate (needed by resampy/soxr)
try:
import ctypes.util
if not ctypes.util.find_library('samplerate'):
missing_deps.append(('libsamplerate', 'audio resampling'))
except:
pass
# Check for portaudio (needed for sounddevice)
try:
import ctypes.util
if not ctypes.util.find_library('portaudio'):
missing_deps.append(('portaudio', 'voice recording'))
except:
pass
if missing_deps:
self.log("Missing system dependencies detected!", "WARNING")
print("\n" + "="*60)
print("MACOS SYSTEM DEPENDENCIES REQUIRED")
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("Then run this install script again.\n")
return False
self.log("macOS system dependencies check passed", "SUCCESS")
return True
def check_linux_dependencies(self):
"""Check for required 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("Missing system dependencies detected!", "WARNING")
print("\n" + "="*60)
print("LINUX SYSTEM DEPENDENCIES REQUIRED")
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_deps = deps_list.replace('-dev', '-devel').replace('19', '')
print(f"sudo dnf install {fedora_deps}")
print("="*60)
print("Then run this install script again.\n")
return False
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:
self.log(f"Warning: {description} failed (continuing anyway): {e.stderr.strip()}", "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 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 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",
]
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>=4.51.3", # Required for VibeVoice compatibility
# 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",
"opencv-python",
"pillow",
# 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)
"gradio", # Required by VibeVoice (may already be available)
"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)
self.run_pip_command(["install", "monotonic-alignment-search"], "Installing monotonic-alignment-search", ignore_errors=True)
# Smart faiss installation: GPU on Linux with CUDA, CPU fallback for Windows/compatibility
cuda_version = self.detect_cuda_version()
if not self.is_windows and cuda_version != "cpu":
# Linux with CUDA - try GPU acceleration
self.log("Linux + CUDA detected - attempting faiss-gpu for better RVC performance", "INFO")
try:
# Determine CUDA version for faiss-gpu package
if cuda_version in ["cu121", "cu124"]: # CUDA 12.x
faiss_gpu_package = "faiss-gpu-cu12>=1.7.4"
elif cuda_version == "cu118": # CUDA 11.x
faiss_gpu_package = "faiss-gpu-cu11>=1.7.4"
else:
# Fallback for other CUDA versions
faiss_gpu_package = "faiss-gpu-cu12>=1.7.4"
# Try GPU installation first with --no-deps to prevent numpy downgrade
self.run_pip_command(["install", "--no-deps", faiss_gpu_package], f"Installing {faiss_gpu_package} for GPU acceleration (--no-deps)")
self.log("faiss-gpu installed with --no-deps - RVC will use GPU acceleration without downgrading numpy", "SUCCESS")
except subprocess.CalledProcessError:
# GPU installation failed - fallback to CPU
self.log("faiss-gpu installation failed - falling back to CPU version", "WARNING")
self.run_pip_command(["install", "faiss-cpu>=1.7.4"], "Installing faiss-cpu (fallback)")
else:
# Windows or no CUDA - use reliable CPU version
if self.is_windows and cuda_version != "cpu":
self.log("Windows + CUDA detected - faiss-gpu not available on Windows, using CPU version", "INFO")
else:
self.log("No CUDA detected - using faiss-cpu", "INFO")
self.run_pip_command(["install", "faiss-cpu>=1.7.4"], "Installing faiss-cpu for RVC voice matching")
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
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_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
"torchcrepe", # Conflicts via librosa dependency
"onnxruntime", # For OpenSeeFace, but forces numpy 2.3.x
"opencv-python", # Forces numpy downgrade from 2.x to 1.26.x
]
for package in problematic_packages:
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
vibevoice_deps = [
"aiortc", # Audio/video real-time communication - safe to install
"pyee", # Event emitter - lightweight
"dnspython", # DNS toolkit - safe
"ifaddr", # Network interface addresses - safe
"pylibsrtp", # SRTP library - safe
"pyopenssl", # OpenSSL wrapper - safe
]
self.log("Installing VibeVoice safe dependencies first", "INFO")
for dep in vibevoice_deps:
self.run_pip_command(
["install", dep],
f"Installing {dep}",
ignore_errors=True
)
# 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")
self.run_pip_command(
["install", "git+https://github.com/FushionHub/VibeVoice.git", "--no-deps"],
"Installing VibeVoice (--no-deps)",
ignore_errors=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")
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")
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)
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 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")
# Ensure onnxruntime is available for OpenSeeFace (with --no-deps to avoid conflicts)
self.run_pip_command(
["install", "onnxruntime", "--no-deps", "--force-reinstall"],
"Installing onnxruntime for OpenSeeFace (Python 3.13)",
ignore_errors=True
)
def validate_installation(self):
"""Validate that critical packages can be imported"""
self.log("Validating installation...", "INFO")
critical_imports = [
("torch", "PyTorch"),
("torchaudio", "TorchAudio"),
("transformers", "Transformers"),
("soundfile", "SoundFile"),
("numpy", "NumPy"),
("librosa", "Librosa"),
("omegaconf", "OmegaConf")
]
validation_errors = []
for module_name, display_name in critical_imports:
try:
__import__(module_name)
self.log(f"{display_name}: OK", "SUCCESS")
except ImportError as e:
validation_errors.append(f"{display_name}: {e}")
self.log(f"{display_name}: FAILED - {e}", "ERROR")
# Check Python 3.13 specific validations
if self.is_python_313:
try:
import onnxruntime
self.log("ONNXRuntime (OpenSeeFace): OK", "SUCCESS")
except ImportError:
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:
try:
__import__(module_name)
self.log(f"{display_name} (RVC): OK", "SUCCESS")
except ImportError:
# 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:
import numpy
numpy_version = tuple(map(int, numpy.__version__.split('.')[:2]))
if numpy_version >= (2, 3):
self.log(f"WARNING: NumPy {numpy.__version__} 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__}: Version OK for compatibility", "SUCCESS")
except ImportError:
self.log("NumPy not found - this will cause issues", "ERROR")
def print_installation_summary(self):
"""Print installation summary and next steps"""
print("\n" + "="*70)
print(" "*20 + "TTS AUDIO SUITE INSTALLATION")
print("="*70)
self.log("Installation completed successfully!", "SUCCESS")
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(" [+] All TTS engines: WORKING")
print(" (ChatterBox, F5-TTS, Higgs Audio)")
print(" [+] RVC voice conversion: WORKING")
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(" FULL COMPATIBILITY STATUS")
print("-"*50)
print(" [+] All TTS engines: WORKING")
print(" [+] RVC voice conversion: WORKING")
print(" [+] MediaPipe mouth movement: WORKING")
print(" [+] OpenSeeFace mouth movement: WORKING")
print("\n" + "="*70)
print(" "*15 + "READY TO USE TTS AUDIO SUITE IN COMFYUI!")
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()
installer.ensure_requirements_installed() # Ensure requirements.txt is installed first
# Check system dependencies (Linux only)
if not installer.check_system_dependencies():
installer.log("System dependency check failed - aborting installation", "ERROR")
sys.exit(1)
# 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_problematic_packages()
installer.install_vibevoice() # Install VibeVoice with careful dependency management
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.handle_wandb_issues() # Fix wandb circular import
installer.handle_python_313_specific()
# Validation and summary
installer.check_version_conflicts()
success = installer.validate_installation()
installer.print_installation_summary()
if not success:
installer.log("Installation completed with warnings - some features may not work", "WARNING")
sys.exit(1)
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()