Files
diodiogod-TTS-Audio-Suite/__init__.py
T
diodiogod e840c6df4d Version 4.19.2
Add configurable VC engine for <restore> tags

Technical implementation:
- Add ComfyUI settings menu option for VC engine selection
- Support three VC engines: ChatterBox 23-Lang, ChatterBox, CosyVoice
- Fix module import to use shared sys.modules instance
- Implement file-based settings cache for persistence across restarts
- Add beforeQueuing hook to ensure settings sync before workflows
2026-01-28 12:18:54 -03:00

344 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
TTS Audio Suite - Universal multi-engine TTS extension for ComfyUI
Unified architecture supporting ChatterBox, F5-TTS, and future engines like RVC:
• 🎤 TTS Text (unified text-to-speech)
• 📺 TTS SRT (unified SRT subtitle timing)
• 🔄 Voice Changer (unified voice conversion)
• ⚙️ Engine nodes (ChatterBox, F5-TTS)
• 🎭 Character Voices (voice reference management)
"""
# Import from the main nodes.py file which handles the new unified architecture
import importlib.util
import os
import sys
# CRITICAL: Apply compatibility patches BEFORE any other imports
#
# Timing is critical: The monkey-patches must be applied BEFORE libraries are imported
# by any other module. If we wait until after other imports, they will already
# be loaded and our monkey-patches won't affect the already-imported module references.
#
# PyTorch patches solve TWO PyTorch 2.9 issues:
# 1. TorchCodec DLL incompatibility on Windows - Global patch uses scipy instead
# 2. PyTorch 2.9's changed torchaudio.load() returning raw int16 - safe_load_audio() normalizes
#
# Transformers patches solve:
# 1. Step Audio EditX tokenization bug in transformers 4.54+ (audio tokens not recognized)
# 2. Various model compatibility issues
try:
# Load pytorch_patches directly by file path to avoid package import issues
pytorch_patches_path = os.path.join(os.path.dirname(__file__), "utils", "compatibility", "pytorch_patches.py")
spec = importlib.util.spec_from_file_location("pytorch_patches_module", pytorch_patches_path)
pytorch_patches_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(pytorch_patches_module)
# Apply the patches (will only apply on PyTorch 2.9+, silently skip on older versions)
pytorch_patches_module.apply_pytorch_patches(verbose=True)
except Exception as e:
print(f"⚠️ Warning: Could not apply PyTorch patches: {e}")
# Apply transformers compatibility patches
try:
# Load transformers_patches directly by file path
transformers_patches_path = os.path.join(os.path.dirname(__file__), "utils", "compatibility", "transformers_patches.py")
spec = importlib.util.spec_from_file_location("transformers_patches_module", transformers_patches_path)
transformers_patches_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(transformers_patches_module)
# Apply the patches (will only apply on transformers 4.54+, silently skip on older versions)
transformers_patches_module.apply_transformers_patches(verbose=True)
except Exception as e:
print(f"⚠️ Warning: Could not apply Transformers patches: {e}")
# Smart Numba Compatibility System - tests and applies fixes only when needed
try:
# Load numba_compat directly by file path to avoid package import issues
numba_compat_path = os.path.join(os.path.dirname(__file__), "utils", "compatibility", "numba_compat.py")
spec = importlib.util.spec_from_file_location("numba_compat_module", numba_compat_path)
numba_compat_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(numba_compat_module)
# Apply smart compatibility setup (fast startup test)
compatibility_results = numba_compat_module.setup_numba_compatibility(quick_startup=True, verbose=False)
except Exception:
# Fallback to simple approach if compatibility module not found
import sys
import os
if sys.version_info >= (3, 13):
# Basic test: try librosa.stft and apply workaround if it fails
try:
import numpy as np
import librosa
test_audio = np.random.randn(512).astype(np.float32)
_ = librosa.stft(test_audio, hop_length=256, n_fft=512)
# Only show when there's a problem, not success
# Mark that we've tested numba compatibility
import sys
sys.modules['__main__']._tts_numba_tested = True
except Exception as e:
if "'function' object has no attribute 'get_call_template'" in str(e):
os.environ['NUMBA_DISABLE_JIT'] = '1'
os.environ['NUMBA_ENABLE_CUDASIM'] = '1'
try:
import numba
numba.config.DISABLE_JIT = True
except ImportError:
pass
print("🔧 Applied numba JIT workaround for Python 3.13 compatibility")
else:
print(f"⚠️ Librosa test failed with different error: {e}")
else:
# Only show warning when JIT is disabled (indicates a problem)
pass
# TorchCodec note: Removed torchcodec dependency to eliminate FFmpeg system requirement
# torchaudio.load() works fine with fallback backends (soundfile, scipy)
import warnings
import sys
import os
# Version disclosure for troubleshooting
def print_critical_versions():
"""Print versions of critical packages for troubleshooting"""
critical_packages = [
('numpy', 'NumPy'),
('librosa', 'Librosa'),
('numba', 'Numba'),
('torch', 'PyTorch'),
('torchaudio', 'TorchAudio'),
('transformers', 'Transformers'),
('accelerate', 'Accelerate'),
('soundfile', 'SoundFile'),
]
version_info = []
for pkg_name, display_name in critical_packages:
try:
module = __import__(pkg_name)
version = getattr(module, '__version__', 'unknown')
version_info.append(f"{display_name} {version}")
except ImportError:
version_info.append(f"{display_name} not installed")
print(f"ℹ️ Critical package versions: {', '.join(version_info)}")
def check_ffmpeg_availability():
"""Check ffmpeg availability and log status"""
try:
# Load ffmpeg_utils directly by file path to avoid package import issues
ffmpeg_utils_path = os.path.join(os.path.dirname(__file__), "utils", "ffmpeg_utils.py")
spec = importlib.util.spec_from_file_location("ffmpeg_utils_module", ffmpeg_utils_path)
ffmpeg_utils_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(ffmpeg_utils_module)
if ffmpeg_utils_module.FFmpegUtils.is_available():
# Only show when unavailable (problem)
pass
else:
print("⚠️ FFmpeg not found - using fallback audio processing (reduced quality)")
print("💡 Install FFmpeg for optimal performance: https://ffmpeg.org/download.html")
except ImportError:
# Fallback check if utils not available yet
try:
import subprocess
result = subprocess.run(['ffmpeg', '-version'], capture_output=True, timeout=5)
if result.returncode == 0:
# Only show when unavailable (problem)
pass
else:
print("⚠️ FFmpeg not found - using fallback audio processing (reduced quality)")
except Exception:
print("⚠️ FFmpeg not found - using fallback audio processing (reduced quality)")
print("💡 Install FFmpeg for optimal performance: https://ffmpeg.org/download.html")
# Print versions and check dependencies immediately for troubleshooting
print_critical_versions()
check_ffmpeg_availability()
# Check for old ChatterBox extension conflict
def check_old_extension_conflict():
"""Check if the old ComfyUI_ChatterBox_SRT_Voice extension is installed"""
try:
import folder_paths
custom_nodes_path = folder_paths.get_folder_paths("custom_nodes")[0]
old_extension_path = os.path.join(custom_nodes_path, "ComfyUI_ChatterBox_SRT_Voice")
if os.path.exists(old_extension_path):
print("\n" + "="*80)
print("⚠️ EXTENSION CONFLICT DETECTED ⚠️")
print("="*80)
print("❌ OLD EXTENSION FOUND: ComfyUI_ChatterBox_SRT_Voice")
print("🆕 CURRENT EXTENSION: ComfyUI_TTS_Audio_Suite")
print("")
print("The old 'ComfyUI_ChatterBox_SRT_Voice' extension conflicts with this")
print("new 'ComfyUI_TTS_Audio_Suite' extension and MUST be removed.")
print("")
print("REQUIRED ACTION:")
print(f"1. Delete the old extension folder: {old_extension_path}")
print("2. Restart ComfyUI")
print("")
print("The TTS Audio Suite is the evolved version with:")
print("• Unified architecture supporting multiple TTS engines")
print("• Better performance and stability")
print("• All features from the old extension plus new capabilities")
print("")
print("Your workflows will be compatible - just update node names.")
print("="*80)
print("")
return True
except Exception as e:
# Silently continue if we can't check (e.g., folder_paths not available yet)
pass
return False
# Perform conflict check
OLD_EXTENSION_CONFLICT = check_old_extension_conflict()
# CRITICAL FIX FOR ISSUE #191: Clear poisoned utils from sys.modules
# Some custom nodes (e.g., LG_HotReload) have a utils.py file that gets loaded
# into sys.modules['utils'], shadowing our utils/ directory package.
# This causes "No module named 'utils.models'; 'utils' is not a package" errors
# when our code tries to import from utils submodules.
# We must clear it BEFORE loading nodes.py which imports from utils.
if 'utils' in sys.modules:
utils_module = sys.modules['utils']
# Check if it's a poisoned utils (single .py file, not a package directory)
# Real packages have __path__ attribute, single files don't
if not hasattr(utils_module, '__path__'):
# It's a single .py file masquerading as utils - this will break our imports
utils_file = getattr(utils_module, '__file__', 'unknown')
print(f"\n{'='*80}")
print(f"⚠️ UTILS NAMESPACE CONFLICT DETECTED")
print(f"{'='*80}")
print(f"Another custom node has a 'utils.py' file in sys.modules['utils']:")
print(f" Source: {utils_file}")
print(f"")
print(f"This conflicts with TTS Audio Suite's 'utils/' package directory.")
print(f"Removing the conflicting module to allow TTS Audio Suite to load.")
print(f"")
print(f"If this causes issues with another custom node, that node should:")
print(f"• Use relative imports (from .utils import X)")
print(f"• Or use a unique name instead of 'utils'")
print(f"{'='*80}\n")
# Delete the poisoned utils module and any attempted submodules
del sys.modules['utils']
to_delete = [key for key in sys.modules.keys() if key.startswith('utils.')]
for key in to_delete:
del sys.modules[key]
# Get the path to the nodes.py file
nodes_py_path = os.path.join(os.path.dirname(__file__), "nodes.py")
# Load nodes.py as a module
spec = importlib.util.spec_from_file_location("nodes_main", nodes_py_path)
nodes_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(nodes_module)
# Import constants and utilities
IS_DEV = nodes_module.IS_DEV
VERSION = nodes_module.VERSION
SEPARATOR = nodes_module.SEPARATOR
VERSION_DISPLAY = nodes_module.VERSION_DISPLAY
# The new unified architecture handles all node registration in nodes.py
# Just import the mappings that nodes.py creates
NODE_CLASS_MAPPINGS = nodes_module.NODE_CLASS_MAPPINGS
NODE_DISPLAY_NAME_MAPPINGS = nodes_module.NODE_DISPLAY_NAME_MAPPINGS
# Extension info
__version__ = VERSION_DISPLAY
__author__ = "TTS Audio Suite"
__description__ = "Universal multi-engine TTS extension for ComfyUI with unified architecture supporting ChatterBox, F5-TTS, and future engines like RVC"
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
# Define web directory for JavaScript files (settings UI)
WEB_DIRECTORY = "./web"
# Register API endpoint for widget data
def setup_api_routes():
"""Setup API routes for widget communication"""
try:
import json
from server import PromptServer
from aiohttp import web
@PromptServer.instance.routes.get("/api/tts-audio-suite/available-characters")
async def get_available_characters_endpoint(request):
"""API endpoint to get available TTS character voices including aliases"""
try:
# Load voice discovery directly by file path to avoid package import issues
voice_discovery_path = os.path.join(os.path.dirname(__file__), "utils", "voice", "discovery.py")
spec = importlib.util.spec_from_file_location("voice_discovery_module", voice_discovery_path)
voice_discovery_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(voice_discovery_module)
characters = list(voice_discovery_module.get_available_characters())
# Also get character aliases
aliases = list(voice_discovery_module.voice_discovery._character_aliases.keys()) if hasattr(voice_discovery_module.voice_discovery, '_character_aliases') else []
# Combine and deduplicate
all_chars = sorted(set(characters + aliases))
return web.json_response({"characters": all_chars})
except Exception as e:
print(f"⚠️ Error retrieving available characters: {e}")
return web.json_response({"characters": [], "error": str(e)})
@PromptServer.instance.routes.get("/api/tts-audio-suite/available-languages")
async def get_available_languages_endpoint(request):
"""API endpoint to get available language codes from the canonical language mapper"""
try:
# Load language_mapper directly by file path to avoid package import issues
language_mapper_path = os.path.join(os.path.dirname(__file__), "utils", "models", "language_mapper.py")
spec = importlib.util.spec_from_file_location("language_mapper_module", language_mapper_path)
language_mapper_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(language_mapper_module)
# Get all unique canonical language codes (the values in LANGUAGE_ALIASES)
languages = sorted(set(language_mapper_module.LANGUAGE_ALIASES.values()))
return web.json_response({"languages": languages})
except Exception as e:
print(f"⚠️ Error retrieving available languages: {e}")
# Fallback list
return web.json_response({"languages": ["en", "de", "fr", "ja", "es", "it", "pt", "th", "no"], "error": str(e)})
@PromptServer.instance.routes.post("/api/tts-audio-suite/settings")
async def set_inline_tag_settings_endpoint(request):
"""API endpoint to receive settings from frontend for inline edit tags and restore VC"""
print("🔧 Settings endpoint called") # Immediate print to verify endpoint is reached
try:
data = await request.json()
precision = data.get("precision", "auto")
device = data.get("device", "auto")
vc_engine = data.get("vc_engine", "chatterbox_23lang")
print(f"🔧 Received settings: precision={precision}, device={device}, vc_engine={vc_engine}")
# Import edit_post_processor using normal import to ensure we get the same module instance
# that will be used during workflow execution
# CRITICAL: Must use the same module instance, not create a new one via importlib!
try:
from utils.audio import edit_post_processor as edit_post_processor_module
except ImportError:
# Fallback: Load directly by file path if normal import fails
edit_post_processor_path = os.path.join(os.path.dirname(__file__), "utils", "audio", "edit_post_processor.py")
spec = importlib.util.spec_from_file_location("utils.audio.edit_post_processor", edit_post_processor_path)
edit_post_processor_module = importlib.util.module_from_spec(spec)
sys.modules["utils.audio.edit_post_processor"] = edit_post_processor_module # Register in sys.modules!
spec.loader.exec_module(edit_post_processor_module)
# Store in global settings that edit_post_processor can access
edit_post_processor_module.set_inline_tag_settings(precision=precision, device=device, vc_engine=vc_engine)
return web.json_response({"status": "success", "precision": precision, "device": device, "vc_engine": vc_engine})
except Exception as e:
print(f"⚠️ Error setting inline tag settings: {e}")
return web.json_response({"status": "error", "error": str(e)})
except Exception as e:
print(f"⚠️ Could not setup API routes: {e}")
# Setup API routes when extension loads
setup_api_routes()
# nodes.py already handles all the startup output and status reporting