-
-
Notifications
You must be signed in to change notification settings - Fork 101
Expand file tree
/
Copy pathinstall.py
More file actions
1574 lines (1360 loc) · 74.8 KB
/
install.py
File metadata and controls
1574 lines (1360 loc) · 74.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/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
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"]
self.russian_stress_fork_ref = "git+https://github.com/diodiogod/add-stress-to-epub.git@98f53b9"
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 specs (without importing modules)
missing_packages = []
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:
version(package_name)
except PackageNotFoundError:
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:
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 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 _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 required 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("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:
error_msg = e.stderr.strip()
if len(error_msg) > 1000:
error_msg = f"...(last 1000 chars)...\n{error_msg[-1000:]}"
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 verify_python_import(self, module_name: str) -> bool:
"""Verify a module imports in the target Python environment."""
try:
result = subprocess.run(
[sys.executable, "-c", f"import {module_name}"],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode == 0:
return True
error_text = (result.stderr or result.stdout or "unknown import failure").strip()
self.log(f"Python import check failed for {module_name}: {error_text}", "WARNING")
return False
except Exception as e:
self.log(f"Python import check failed for {module_name}: {e}", "WARNING")
return False
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",
]
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,<=4.57.3", # Required for VibeVoice compatibility. 5.0.0 breaks Qwen3-TTS tokenizers.
# 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",
# 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")