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.
1203 lines
58 KiB
Python
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() |