Files
diodiogod-TTS-Audio-Suite/utils/system/import_manager.py
T
diodiogod dffd6f4cab Reduce verbose init logging - remove success messages for FFmpeg, numba JIT, engine loading, and model discovery
- Remove 'FFmpeg available' success message (only show when unavailable)
- Remove 'numba JIT enabled' success message
- Remove 'ChatterboxTTS/VC loaded from bundled package' success messages
- Remove 'F5-TTS loaded from system package' success message
- Remove 'Using ComfyUI ChatterBox models' legacy success message
- Keep important diagnostic messages like 'Failed to load GPU Faiss' and package versions
2025-10-23 22:02:42 -03:00

479 lines
21 KiB
Python

"""
Import Manager - Centralized import handling with fallbacks for ChatterBox Voice
Handles complex import scenarios with bundled vs system packages
"""
import os
import sys
import warnings
import importlib.util
from typing import Optional, Dict, Any, Tuple, List
class ImportManager:
"""
Manages complex import scenarios for ChatterBox Voice components.
Handles bundled vs system imports with proper fallbacks.
"""
def __init__(self, node_dir: Optional[str] = None, is_dev: bool = True):
"""
Initialize ImportManager.
Args:
node_dir: Optional override for the node directory path
is_dev: Whether to show debug messages
"""
self.node_dir = node_dir or os.path.dirname(os.path.dirname(__file__))
self.bundled_chatterbox_dir = os.path.join(self.node_dir, "chatterbox")
self.is_dev = is_dev
# Import status tracking
self.import_status: Dict[str, Dict[str, Any]] = {
"chatterbox_tts": {"available": False, "source": None, "error": None},
"chatterbox_vc": {"available": False, "source": None, "error": None},
"f5tts": {"available": False, "source": None, "error": None},
"srt_parser": {"available": False, "source": None, "error": None},
"audio_timing": {"available": False, "source": None, "error": None},
}
# Loaded modules cache
self.loaded_modules: Dict[str, Any] = {}
def _add_node_dir_to_path(self):
"""Add node directory to Python path for bundled imports."""
if self.node_dir not in sys.path:
sys.path.insert(0, self.node_dir)
def _log_debug(self, message: str):
"""Log debug message if in dev mode."""
if self.is_dev:
try:
print(message)
except UnicodeEncodeError:
# Fallback for Windows console encoding issues
safe_message = message.encode('ascii', 'replace').decode('ascii')
print(safe_message)
def import_chatterbox_tts(self) -> Tuple[bool, Optional[Any], str]:
"""
Import ChatterboxTTS with bundled first, then system fallback.
Returns:
Tuple of (success, module_class, source)
"""
module_key = "chatterbox_tts"
# Return cached if available
if self.import_status[module_key]["available"]:
return True, self.loaded_modules[module_key], self.import_status[module_key]["source"]
# Try bundled first
try:
self._add_node_dir_to_path()
from engines.chatterbox.tts import ChatterboxTTS
self.loaded_modules[module_key] = ChatterboxTTS
self.import_status[module_key] = {
"available": True,
"source": "bundled",
"error": None
}
# Only log on error, not success
pass
return True, ChatterboxTTS, "bundled"
except ImportError as bundled_error:
# Try system installation
try:
from engines.chatterbox.tts import ChatterboxTTS
self.loaded_modules[module_key] = ChatterboxTTS
self.import_status[module_key] = {
"available": True,
"source": "system",
"error": None
}
self._log_debug("✅ ChatterboxTTS loaded from system package")
return True, ChatterboxTTS, "system"
except ImportError as system_error:
self.import_status[module_key] = {
"available": False,
"source": None,
"error": f"Bundled: {bundled_error}, System: {system_error}"
}
self._log_debug(f"❌ ChatterboxTTS not available: {system_error}")
return False, None, "none"
def import_chatterbox_vc(self) -> Tuple[bool, Optional[Any], str]:
"""
Import ChatterboxVC with bundled first, then system fallback.
Returns:
Tuple of (success, module_class, source)
"""
module_key = "chatterbox_vc"
# Return cached if available
if self.import_status[module_key]["available"]:
return True, self.loaded_modules[module_key], self.import_status[module_key]["source"]
# Try bundled first
try:
self._add_node_dir_to_path()
from engines.chatterbox.vc import ChatterboxVC
self.loaded_modules[module_key] = ChatterboxVC
self.import_status[module_key] = {
"available": True,
"source": "bundled",
"error": None
}
# Only log on error, not success
pass
return True, ChatterboxVC, "bundled"
except ImportError as bundled_error:
# Try system installation
try:
from engines.chatterbox.vc import ChatterboxVC
self.loaded_modules[module_key] = ChatterboxVC
self.import_status[module_key] = {
"available": True,
"source": "system",
"error": None
}
self._log_debug("✅ ChatterboxVC loaded from system package")
return True, ChatterboxVC, "system"
except ImportError as system_error:
self.import_status[module_key] = {
"available": False,
"source": None,
"error": f"Bundled: {bundled_error}, System: {system_error}"
}
self._log_debug(f"❌ ChatterboxVC not available: {system_error}")
return False, None, "none"
def import_f5tts(self) -> Tuple[bool, Any, str]:
"""
Import F5-TTS with fallback handling.
Returns:
Tuple of (success, f5tts_module, source)
"""
module_key = "f5tts"
# Return cached if available
if self.import_status[module_key]["available"]:
return True, self.loaded_modules[module_key], self.import_status[module_key]["source"]
try:
# Try to import F5-TTS API
from engines.f5_tts.api import F5TTS
self.loaded_modules[module_key] = F5TTS
self.import_status[module_key] = {
"available": True,
"source": "system",
"error": None
}
# Only log on error, not success
pass
return True, F5TTS, "system"
except ImportError as error:
self.import_status[module_key] = {
"available": False,
"source": None,
"error": str(error)
}
self._log_debug(f"❌ F5-TTS not available: {error}")
return False, None, "none"
def import_srt_modules(self) -> Tuple[bool, Dict[str, Any], str]:
"""
Import SRT-related modules with multiple fallback strategies.
Returns:
Tuple of (success, modules_dict, source)
"""
module_key = "srt_parser"
# Return cached if available
if self.import_status[module_key]["available"]:
return True, self.loaded_modules[module_key], self.import_status[module_key]["source"]
modules = {}
# Strategy 1: Direct file loading from bundled directory
srt_parser_path = os.path.join(self.bundled_chatterbox_dir, 'srt_parser.py')
audio_timing_path = os.path.join(self.bundled_chatterbox_dir, 'audio_timing.py')
if os.path.exists(srt_parser_path) and os.path.exists(audio_timing_path):
try:
# Load SRT parser module directly from file
srt_parser_spec = importlib.util.spec_from_file_location("srt_parser", srt_parser_path)
srt_parser_module = importlib.util.module_from_spec(srt_parser_spec)
srt_parser_spec.loader.exec_module(srt_parser_module)
# Extract SRT parser classes and functions
modules.update({
"SRTParser": srt_parser_module.SRTParser,
"SRTSubtitle": srt_parser_module.SRTSubtitle,
"SRTParseError": srt_parser_module.SRTParseError,
"validate_srt_timing_compatibility": srt_parser_module.validate_srt_timing_compatibility,
})
# Try to load audio timing module
try:
audio_timing_spec = importlib.util.spec_from_file_location("audio_timing", audio_timing_path)
audio_timing_module = importlib.util.module_from_spec(audio_timing_spec)
audio_timing_spec.loader.exec_module(audio_timing_module)
# Extract audio timing classes and functions
modules.update({
"AudioTimingUtils": audio_timing_module.AudioTimingUtils,
"PhaseVocoderTimeStretcher": audio_timing_module.PhaseVocoderTimeStretcher,
"TimedAudioAssembler": audio_timing_module.TimedAudioAssembler,
"calculate_timing_adjustments": audio_timing_module.calculate_timing_adjustments,
"AudioTimingError": audio_timing_module.AudioTimingError,
"FFmpegTimeStretcher": getattr(audio_timing_module, "FFmpegTimeStretcher", None),
})
except Exception as timing_error:
self._log_debug(f"⚠️ Audio timing not fully available: {timing_error}")
# Add fallback implementations
modules.update(self._create_fallback_audio_timing())
self.loaded_modules[module_key] = modules
self.import_status[module_key] = {
"available": True,
"source": "direct_file",
"error": None
}
# SRT modules are bundled - no need to show success message
return True, modules, "direct_file"
except Exception as file_error:
self._log_debug(f"❌ SRT modules failed to load (bundled files corrupted?): {file_error}")
# Strategy 2: Try bundled package import
if os.path.exists(self.bundled_chatterbox_dir):
try:
self._add_node_dir_to_path()
from utils.timing.parser import SRTParser, SRTSubtitle, SRTParseError, validate_srt_timing_compatibility
from engines.chatterbox.audio_timing import (
AudioTimingUtils, PhaseVocoderTimeStretcher, TimedAudioAssembler,
calculate_timing_adjustments, AudioTimingError
)
modules.update({
"SRTParser": SRTParser,
"SRTSubtitle": SRTSubtitle,
"SRTParseError": SRTParseError,
"validate_srt_timing_compatibility": validate_srt_timing_compatibility,
"AudioTimingUtils": AudioTimingUtils,
"PhaseVocoderTimeStretcher": PhaseVocoderTimeStretcher,
"TimedAudioAssembler": TimedAudioAssembler,
"calculate_timing_adjustments": calculate_timing_adjustments,
"AudioTimingError": AudioTimingError,
})
# Try to get FFmpegTimeStretcher
try:
from engines.chatterbox.audio_timing import FFmpegTimeStretcher
modules["FFmpegTimeStretcher"] = FFmpegTimeStretcher
except ImportError:
modules["FFmpegTimeStretcher"] = None
self.loaded_modules[module_key] = modules
self.import_status[module_key] = {
"available": True,
"source": "bundled_package",
"error": None
}
self._log_debug("✅ SRT modules loaded from bundled package")
return True, modules, "bundled_package"
except ImportError as bundled_error:
self._log_debug(f"❌ Failed to load SRT from bundled package: {bundled_error}")
# Strategy 3: Try system package import
try:
from utils.timing.parser import SRTParser, SRTSubtitle, SRTParseError, validate_srt_timing_compatibility
from engines.chatterbox.audio_timing import (
AudioTimingUtils, PhaseVocoderTimeStretcher, TimedAudioAssembler,
calculate_timing_adjustments, AudioTimingError
)
modules.update({
"SRTParser": SRTParser,
"SRTSubtitle": SRTSubtitle,
"SRTParseError": SRTParseError,
"validate_srt_timing_compatibility": validate_srt_timing_compatibility,
"AudioTimingUtils": AudioTimingUtils,
"PhaseVocoderTimeStretcher": PhaseVocoderTimeStretcher,
"TimedAudioAssembler": TimedAudioAssembler,
"calculate_timing_adjustments": calculate_timing_adjustments,
"AudioTimingError": AudioTimingError,
})
# Try to get FFmpegTimeStretcher
try:
from engines.chatterbox.audio_timing import FFmpegTimeStretcher
modules["FFmpegTimeStretcher"] = FFmpegTimeStretcher
except ImportError:
modules["FFmpegTimeStretcher"] = None
self.loaded_modules[module_key] = modules
self.import_status[module_key] = {
"available": True,
"source": "system_package",
"error": None
}
return True, modules, "system_package"
except ImportError as system_error:
self.import_status[module_key] = {
"available": False,
"source": None,
"error": f"All import strategies failed. Last error: {system_error}"
}
self._log_debug(f"❌ SRT modules critically failed to load: {system_error}")
return False, self._create_dummy_srt_modules(), "none"
def _create_fallback_audio_timing(self) -> Dict[str, Any]:
"""Create minimal fallback implementations for audio timing."""
import torch
class AudioTimingUtils:
@staticmethod
def get_audio_duration(audio, sample_rate):
if audio.dim() == 1:
return audio.size(0) / sample_rate
elif audio.dim() == 2:
return audio.size(-1) / sample_rate
else:
raise ValueError(f"Unsupported audio tensor dimensions: {audio.dim()}")
@staticmethod
def create_silence(duration_seconds, sample_rate, channels=1, device=None):
num_samples = int(duration_seconds * sample_rate)
if channels == 1:
return torch.zeros(num_samples, device=device)
else:
return torch.zeros(channels, num_samples, device=device)
@staticmethod
def pad_audio_to_duration(audio, target_duration, sample_rate, pad_type="end"):
current_duration = AudioTimingUtils.get_audio_duration(audio, sample_rate)
if current_duration >= target_duration:
return audio
pad_duration = target_duration - current_duration
silence = AudioTimingUtils.create_silence(
pad_duration, sample_rate,
channels=audio.shape[0] if audio.dim() == 2 else 1,
device=audio.device
)
if pad_type == "end":
return torch.cat([audio, silence], dim=-1)
else:
return torch.cat([silence, audio], dim=-1)
@staticmethod
def seconds_to_samples(seconds, sample_rate):
return int(seconds * sample_rate)
class TimedAudioAssembler:
def __init__(self, sample_rate):
self.sample_rate = sample_rate
def assemble_timed_audio(self, audio_segments, target_timings, fade_duration=0.01):
return torch.cat(audio_segments, dim=-1)
class AudioTimingError(Exception):
pass
def calculate_timing_adjustments(natural_durations, target_timings):
adjustments = []
for i, (natural_duration, (start_time, end_time)) in enumerate(zip(natural_durations, target_timings)):
target_duration = end_time - start_time
stretch_factor = target_duration / natural_duration if natural_duration > 0 else 1.0
adjustments.append({
'segment_index': i,
'natural_duration': natural_duration,
'target_duration': target_duration,
'start_time': start_time,
'end_time': end_time,
'stretch_factor': stretch_factor,
'needs_stretching': abs(stretch_factor - 1.0) > 0.05,
'stretch_type': 'compress' if stretch_factor < 1.0 else 'expand' if stretch_factor > 1.0 else 'none'
})
return adjustments
return {
"AudioTimingUtils": AudioTimingUtils,
"TimedAudioAssembler": TimedAudioAssembler,
"AudioTimingError": AudioTimingError,
"calculate_timing_adjustments": calculate_timing_adjustments,
"PhaseVocoderTimeStretcher": None, # Not available without librosa
"FFmpegTimeStretcher": None, # Not available without FFmpeg
}
def _create_dummy_srt_modules(self) -> Dict[str, Any]:
"""Create dummy SRT modules when imports fail."""
class SRTParser:
@staticmethod
def parse_srt_content(content):
raise ImportError("SRT support not available")
class SRTSubtitle:
def __init__(self, sequence=0, start_time=0.0, end_time=0.0, text=""):
raise ImportError("SRT support not available - missing required modules")
class SRTParseError(Exception):
pass
def validate_srt_timing_compatibility(*args, **kwargs):
raise ImportError("SRT support not available - missing required modules")
# Include fallback audio timing
fallback_audio = self._create_fallback_audio_timing()
return {
"SRTParser": SRTParser,
"SRTSubtitle": SRTSubtitle,
"SRTParseError": SRTParseError,
"validate_srt_timing_compatibility": validate_srt_timing_compatibility,
**fallback_audio
}
def get_import_status(self) -> Dict[str, Dict[str, Any]]:
"""Get current import status for all modules."""
return self.import_status.copy()
def get_availability_summary(self) -> Dict[str, bool]:
"""Get a summary of what's available."""
return {
"tts": self.import_status["chatterbox_tts"]["available"],
"vc": self.import_status["chatterbox_vc"]["available"],
"srt": self.import_status["srt_parser"]["available"],
"any_chatterbox": (
self.import_status["chatterbox_tts"]["available"] or
self.import_status["chatterbox_vc"]["available"]
),
"full_support": all(
self.import_status[key]["available"]
for key in ["chatterbox_tts", "chatterbox_vc", "srt_parser"]
)
}
# Global import manager instance
import_manager = ImportManager()